Текст
                    С^ППТЕР

4 ACTE ЛАОС 1001010014 1001010011101010011 JH 001101100100111 10010100 1*01010011 V 11010100111101001101100100111 К01010110100101100001001100 101001101100100111101010110100
Н. Елманова, С. Трепалин, А. Тенцер Delphi и технология сом МАСТЕР-КЛАСС ПИТЕР Москва Санкт-Петербург Нижний Новгород Воронеж Ростов-на-Дону Екатеринбург - Самара Киев Харьков Минск 2003
ББК 32.973.23-018 УДК 681.3.06 Е52 Е52 Delphi и технология COM (+CD). Мастер-класс / Н. Елманова, С. Трепалин, А. Тендер. — СПб.: Питер, 2003. — 698 с.: ил. ISBN 5-94723-648-6 Книга посвящена использованию технологии Component Object Model (СОМ) в приложениях, созданных с помощью Delphi. Освещаются вопросы, связанные с принципами модели СОМ, раз- работкой элементов управления ActiveX, серверов и контроллеров автоматизации, применением OLE-документов в приложениях, а также с использованием программного обеспечения СОМ и СОМ+ для организации распределенных вычислений. Книга предш значена для опытных программистов, имеющих опыт разработки приложений с помощью Delphi и интересующихся вопросами применения COM-технологии и созданием рас- пределенных приложений на ее основе. ББК 32.973.23-018 УДК 681.3.06 Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 5-94723-648-6 © ЗАО Издательский дом «Питер», 2003
Краткое содержание Благодарности....................................................14 Введение.........................................................15 Глава 1. Основы технологии СОМ ..................................17 Глава 2. Создание элементов управления ActiveX...................91 Глава 3. Создание внепроцессных серверов автоматизации..........131 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office . . 170 Глава 5. Использование OLE-документов в приложениях.............234 Глава 6. Модели потоков и разработка многопоточных приложений...256 Глава 7. Создание внутрипроцессных серверов автоматизации.......299 Глава 8. Создание модулей расширения Microsoft Office...........350 Глава 9. Применение COM-объектов из состава Windows ............386 Глава 10. Microsoft Script Control..............................462 Глава 11. Удаленный доступ к серверам автоматизации.............491 Глава 12. Технология DataSnap...................................518 Глава 13. Создание ASP-объектов.................................600 Глава 14. Службы компонентов....................................632 Вместо заключения...............................................672 Приложение. Инструкция по использованию компакт-диска...........673 Алфавитный указатель ...........................................684
Содержание Благодарности.......................................................14 Введение............................................................15 Для кого написана эта книга.........................................16 Что находится на компакт-диске......................................16 От издательства.....................................................16 Глава 1. Основы технологии СОМ......................................17 Цели и задачи технологии СОМ........................................17 Базовые понятия.....................................................20 Интерфейс........................................................20 СОМ-сервер ......................................................43 СОМ и потоки выполнения..........................................47 Активация сервера................................................48 Поддержка Delphi стандартных интерфейсов СОМ.....................49 Библиотека типов и информация о методах сервера................................................49 Язык IDL.........................................................50 Создание СОМ-сервера................................................52 Сервер без библиотеки типов .....................................55 Сервер с библиотекой типов.......................................58 Создание СОМ-клиента................................................60 Создание модуля расширения в виде СОМ-сервера.......................62 Автоматическая регистрация серверов из приложения ......................................................66 Технология OLE Automation...........................................67 Интерфейс IDispatch .............................................70 Тип данных Variant...............................................71 Диспинтерфейс....................................................72 Дуальные интерфейсы..............................................73 Маршалинг и взаимодействие клиента с сервером.......................74 COM API ............................................................76 Инициализация СОМ................................................77 Управление памятью...............................................78 Создание СОМ-объектов............................................80 Управление загрузкой модулей.....................................83 Функции внутрипроцессного сервера ...............................83
Содержание 7 Маршалинг интерфейсов...........................................84 Работа с идентификаторами GUID..................................87 Заключение.........................................................89 Глава 2. Создание элементов управления ActiveX......................91 Создание элементов управления ActiveX на основе VCL-компонентов....93 Создание страниц свойств........................................... 99 Создание активных форм.............................................102 Создание меню с командами открытия диалоговых окон.................106 Получение информации о контейнере..................................108 Изменение свойств элемента ActiveX в инспекторе объектов..........110 Навигация по web-страницам.........................................111 Изменение свойств элемента управления ActiveX на web-странице......116 Создание обработчиков событий в HTML-документах....................121 Система безопасности Internet Exprorer и цифровая подпись.........123 Динамическая инициализация элементов управления ActiveX............127 Заключение........................................................ 130 Глава 3. Создание внепроцессных серверов автоматизации . . 131 Подготовка приложения для создания сервера автоматизации...........133 Превращение приложения в сервер автоматизации.....................135 Библиотека типов...................................................136 Реализация методов объекта автоматизации...........................139 Тестирование сервера автоматизации................................143 Создание контроллера автоматизации .............................143 Раннее и позднее связывание....................................146 Создание коллекций объектов........................................152 Экспонируемые свойства и методы...................................156 Нотификационные сообщения во внепроцессных серверах...............159 Заключение........................................................168 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office.......................................170 Объектные модели Microsoft Office.................................171 Общие принципы создания контроллеров автоматизации Microsoft Office . ... 172 Автоматизация Microsoft Word.......................................175 Программные идентификаторы и объектная модель Microsoft Word .... 175 Создание и открытие документов Microsoft Word...................176 Сохранение, печать и закрытие документов Microsoft Word.........176 Вставка текста и объектов в документ и форматирование текста....178 Перемещение курсора по тексту ..................................180 Создание таблиц ................................................181 Обращение к свойствам документа.................................182 Автоматизация Microsoft Excel......................................183 Программные идентификаторы и объектная модель Microsoft Excel .... 184 Запуск Microsoft Excel, создание и открытие рабочих книг.......185 Сохранение, печать и закрытие рабочих книг Microsoft Excel......186 Обращение к листам и ячейкам...................................187
8 Содержание Создание диаграмм...............................................189 Применение средств доступа к данным.............................190 Автоматизация Microsoft PowerPoint.................................192 Программные идентификаторы и объектная модель Microsoft PowerPoint 193 Запуск Microsoft PowerPoint, создание и открытие презентаций....194 Сохранение, печать и закрытие презентаций Microsoft PowerPoint..195 Оформление презентаций..........................................196 Манипуляция отдельными слайдами ................................198 Демонстрация слайдов............................................200 Автоматизация Microsoft Outlook....................................202 Программные идентификаторы и объектная модель Microsoft Outlook . . . 203 Запуск Microsoft Outlook, открытие и создание папок.............204 Манипуляция элементами папок....................................205 Манипуляция сообщениями электронной почты.......................206 Манипуляция контактами..........................................208 Манипуляция заметками и задачами................................209 Создание отчетов по базам данных с помощью приложений Office.......210 Генерация отчетов с помощью Microsoft Word .....................211 Генерация отчетов с помощью Microsoft Excel ....................215 Построение диаграмм в отчетах...................................217 Применение коллекций...............................................227 Применение раннего связывания......................................231 Заключение.........................................................232 Глава 5. Использование OLE-документов в приложениях .... 234 Создание и отображение OLE-документов в формах.....................234 Управление объектом внутри OLE-контейнера..........................238 Хранение OLE-объектов в базах данных...............................242 Использование временного файла .................................243 Использование памяти и методов-наследников класса TDataSet......244 Создание OLE-контейнера в виде чувствительного к данным VCL-компонента..................................................246 Заключение.........................................................254 Глава 6. Модели потоков и разработка многопоточных приложений ........................................................256 Класс TThread......................................................257 Понятие о синхронизации............................................261 Потоки и апартаменты...............................................266 STA .............................................................266 МТА.............................................................266 Нейтральный апартамент..........................................267 Передача интерфейсов и параметров...............................267 Инициализация СОМ ............................................. 268 Синхронизация процессов ............................................272 Функции синхронизации......................................... 273 Объекты синхронизации...........................................279 Дополнительные механизмы синхронизации..........................288
Содержание 9 Взаимная блокировка...............................................292 Потокозащищенные классы Delphi....................................296 Заключение........................................................297 Глава 7. Создание внутрипроцессных серверов автоматизации 299 Создание и использование динамически загружаемых библиотек........299 Преимущества реализации кода в DLL.............................299 Создание простейшей библиотеки.................................300 Статическая и динамическая загрузка DLL........................304 Обмен данными с DLL............................................307 Вызов в DLL функций приложения.................................313 Работа с объектами в DLL.......................................317 Модальные формы в DLL..........................................318 Немодальные формы в DLL........................................323 Экспорт дочерних форм из DLL...................................328 Внутрипроцессный сервер автоматизации.............................331 Обработка ошибок .................................................334 Соглашение о вызовах safecall на клиенте ......................335 Соглашение о вызовах safecall на сервере.......................336 Тестовая программа.............................................336 Нотификационные сообщения.........................................343 Заключение........................................................349 Глава 8. Создание модулей расширения Microsoft Office . . . 350 Модель модулей расширения Microsoft Office 2000 ................. 350 Интерфейс IDTExtensibility2.......................................351 Внедрение в объектную модель Office...............................352 События СОМ.......................................................353 Базовый класс обработчика СОМ-событий..........................353 Обработчик событий объекта CommandBarButton....................356 Регистрация модулей расширения....................................357 Разработка модуля расширения......................................358 Библиотеки типов Office 2000 ................................. 358 Создание СОМ-сервера...........................................360 Отладка модулей расширения.....................................362 Реализация функциональности....................................362 Написание надстроек, работающих с несколькими приложениями Office . 367 Создание смарт-тегов для Office ХР................................367 Понятие смарт-тегов ...........................................367 Требования к библиотекам, реализующим смарт-теги...............370 Создание распознавателей смарт-тегов ..........................371 Создание обработчика смарт-тега ...............................376 Поставка и тестирование библиотек, реализующих смарт-теги......379 Заключение........................................................384 Глава 9. Применение СОМ-объектов из состава Windows . . . 386 Создание ярлыков..................................................386 Получение уведомлений от Windows Explorer.........................388
10 Содержание Создание окон просмотра данных в Windows Explorer....................390 Реализация метода перетаскивания.....................................402 Реализация контейнера.............................................402 Реализация источника данных.......................................407 Использование Microsoft Internet Explorer в приложениях..............419 Базовые операции..................................................419 Тонкая настройка..................................................424 Доступ к документной модели TWebBrowser ..........................430 Автозавершение при вводе данных......................................437 Механизм работы...................................................437 Получение списка истории .........................................439 Целевая операционная система......................................440 Реализация компонента lEnumString.................................440 Спецификации компонента...........................................442 Замечания по реализации...........................................444 Создание компонента...............................................444 Использование интерфейсов lACList и IACList2......................448 Выбор целевой папки для навигации.................................449 Создание списков истории из нескольких источников ................450 Тестовая программа................................................450 Добавление вкладок в диалоговое окно свойств файла...................451 Механизм работы...................................................451 Создание СОМ-сервера..............................................454 Создание описания диалогового окна и диалоговой функции...........456 Регистрация расширения оболочки...................................458 Заключение...........................................................460 Глава 10. Microsoft Script Control...................................462 Добавление компонента TScriptControl в программу.....................462 Интеграция компонента TScriptControl с VCL...........................464 Модель расширения компонента TScriptControl.......................465 Интерфейс IDispatch...............................................466 Метод GetldsOfNames...............................................466 Метод Invoke......................................................467 Информация RTTI Delphi............................................468 Класс TVCLProxy...................................................468 Написание метода GetldsOfNames....................................470 Написание метода Invoke...........................................471 Оператор For Each....................................................485 Интерфейс lEnumVariant............................................485 Класс TVCLEnumerator..............................................486 Компонент TVCLScriptControl..........................................489 Заключение...........................................................490 Глава 11. Удаленный доступ к серверам автоматизации . . . 491 Маршалинг и удаленный доступ к СОМ-серверам..........................491 Удаленный доступ с помощью сервисов DCOM.............................493 Настройка доступа.................................................493 Применение компонента TDCOMConnection.............................499
Содержание 11 Удаленный доступ с помощью протокола TCP/IP ........................503 Borland Socket Server............................................503 Применение компонента TSocketConnection .........................505 Безопасность передаваемых данных при работе с компонентом TSocketConnection................................................507 Удаленный доступ с помощью протокола HTTP...........................511 Применение брокеров.................................................513 Заключение..........................................................515 Глава 12. Технология DataSnap.......................................518 Информационные системы..............................................518 Состав...........................................................518 Типичные проблемы................................................519 Способы решения проблем..........................................521 Введение в технологию DataSnap......................................522 Создание простейшего DataSnap-приложения............................525 Создание сервера.................................................525 Создание клиента.................................................528 Модель Briefcase....................................................531 Многопользовательская обработка данных в распределенных системах .... 532 Создание клиентских приложений в виде активных форм.................535 Создание клиента в виде элемента управления ActiveX..............535 Проблемы отображения клиентских приложений в браузерах...........538 Дополнительные возможности DataSnap-приложений .....................539 Создание связи «один ко многим» в технологии DataSnap............539 Использование запросов в DataSnap-приложениях....................543 Использование нескольких модулей данных на сервере доступа к данным 544 Обращение к компонентам VCL из кода удаленного модуля данных .... 548 Перенос бизнес-правил в клиентское приложение....................549 Сортировка данных в компоненте TCIientDataSet....................550 Работа с библиотеками типов.........................................552 Аутентификация пользователей.....................................553 Передача текстовых сообщений от клиента к серверу доступа к данным . . . 556 Нотификации в технологии DataSnap...................................558 Использование технологии DataSnap в однозвенных системах............565 Создание упрощенного приложения для работы с базами данных.......565 Приемы экономии места на форме...................................568 Сохранение содержимого таблиц в локальных файлах.................569 Исторический экскурс................................................572 Переход от Delphi 4 к Delphi 5...................................572 Переход от Delphi 5 к Delphi 6...................................583 Новые компоненты Delphi 6 для создания DataSnap-приложений.......583 Переход от Delphi 6 к Delphi 7...................................585 Реализация DataSnap-серверов как сервисов Windows NT/2000.......... 585 Заключение..........................................................597 Глава 13. Создание ASP-объектов ....................................600 Иерархия ASP-объектов...............................................600 Объект Request...................................................600
12 Содержание Объект Response .................................................601 Объект Server....................................................603 Объект Session...................................................603 Объект Application...............................................605 Работа с ASP-сервером...............................................605 Создание простейшего ASP-сервера....................................607 Использование HTML-форм в ASP-сервере...............................611 Доступ к базам данных в ASP-сервере.................................615 Дополнительные возможности ASP-сервера..............................619 Хранение информации о состоянии.....................................621 Создание внепроцессных ASP-серверов.................................629 Заключение..........................................................630 Глава 14. Службы компонентов........................................632 Назначение служб компонентов........................................632 Принципы работы служб компонентов...................................634 Организация пулов объектов и ресурсов............................635 Управление транзакциями..........................................635 Вопросы безопасности .............................................637 Особенности объектов СОМ+...........................................638 Требования к объектам С0М+.......................................638 Особенности управления объектами СОМ+.............................639 Классы Delphi для создания объектов..............................639 Создание серверных объектов.........................................640 Создание объекта СОМ+ для доступа к данным........................641 Тестирование объекта СОМ+ для доступа к данным ...................646 Управление транзакциями..............................................649 Реализация транзакций............................................650 Тестирование транзакций...........................................656 Управление распределенными транзакциями...........................660 События СОМ+........................................................661 Механизм уведомления о событиях в службах компонентов ............661 Создание объекта-издателя.........................................663 Создание объекта-подписчика с помощью Delphi 6....................665 Тестирование уведомлений о событиях...............................667 Создание объекта-подписчика с помощью Delphi 7....................668 Заключение...........................................................670 Вместо заключения...................................................672 Приложение. Инструкция по использованию компакт-диска . . . 673 Глава 1 .............................................................673 COMPIugins.......................................................673 Глава 3..............................................................673 Глава 4..............................................................674 Word_Rpt..........................................................674 Excel_Rpt.........................................................674 ExcelQueryTable...................................................674 IconExtractor.....................................................675
Содержание 13 Глава 5 .................................................................675 Глава? ..................................................................676 Глава 8 .................................................................676 Addins................................................................676 SmartTags.............................................................676 Глава 9 .................................................................676 ShellFolder...........................................................676 DirChangeHook.........................................................677 AutoComplete .........................................................677 WebBrowser............................................................677 PropertySheet.........................................................677 Глава 11.................................................................677 DCOM_Controller.......................................................677 Socket_Controller.....................................................677 Intercept.............................................................678 ObjectJSroker.........................................................679 Глава 12.................................................................680 lnteractive_Clients ..................................................680 NTSvc.................................................................680 DataSnap..............................................................680 Глава 13.................................................................681 Глава 14.................................................................682 Transactions..........................................................682 Events................................................................683 Алфавитный указатель.....................................................684
Благодарности В работе и над первым, и над вторым изданием этой книги у пас было много по- мощников. В первую очередь мы благодарны главному редактору издательства «Питер» Екатерине Строгановой, в течение двух лет убеждавшей и, наконец, убедившей пас написать книгу для профессиональных разработчиков, а также за- ведующему компьютерной редакцией Илье Корнееву и руководителю проекта первого издания Игорю Жаркову за практическое претворение этого проекта в жизнь. Особую благодарность мы выражаем редактору Алексею Жданову, благо- даря добросовестности, настойчивости, неисчерпаемой фантазии и чувству юмора которого работа над обоими изданиями этой книги стала для пас поистине неза- бываемой, и руководителю проекта второго издания Владимиру Рычкову, ини- циировавшему это издание. Мы также благодарим всех сотрудников издательст- ва «Питер», отвечающих за корректуру, верстку и оформление книги. Мы благодарны нашим читателям, приславшим нам после выхода первого издания свои замечания и предложения, и в особенности Федору Григорьевичу Иваненко. Мы постарались по возможности их учесть при подготовке второго издания. Мы выражаем искреннюю признательность разработчикам Borland Software Corporation за создание такого великолепного продукта, как Delphi, и разработ- чикам Microsoft за создание такой замечательной технологии, как СОМ. И, наконец, мы благодарны нашим семьям за поддержку и понимание, прояв- ленные во время работы над этой книгой. Наталия Елманова, Сергей Трепалин, Анатолий Тенцер
Введение Данная книга посвящена применению технологии Microsoft COM (Component Object Model) в приложениях, созданных с помощью Borland Delphi — одного из самых популярных в нашей стране средств разработки приложений. Будучи не- отъемлемой частью операционных систем семейства Windows и реализуя кон- цепцию объектно-ориентированного подхода на уровне приложений и операци- онной системы, технология СОМ позволяет создавать решения, очень нужные современным пользователям, причем подобные решения подчас довольно слож- но или даже невозможно реализовать иными способами. Сейчас российским читателям доступно немало книг о Delphi, и многие из них затрагивают некоторые аспекты применения СОМ. Однако, как показывает наш собственный опыт и опыт многих других российских разработчиков, с кото- рыми нам довелось общаться, сейчас всем очень не хватает литературы, специ- ально посвященной именно этой стороне использования Delphi и описывающей создание COM-приложений, наиболее часто применяющихся на практике. По- этому мы решили в определенной степени восполнить этот пробел, написав эту книгу. С ее помощью вы узнаете: как создавать, тестировать и применять элементы управления ActiveX; как использовать в приложениях OLE-документы; как создавать серверы и контроллеры автоматизации, в том числе контрол- леры Microsoft Office; как расширить функциональность приложений Microsoft Office с помощью собственных модулей расширения и смарт-тегов; как использовать COM-серверы, входящие в состав операционных систем се- мейства Windows, и добавлять в свои приложения встроенный язык програм- мирования; Я как организовать распределенную обработку данных с помощью технологии DataSnap (ранее знакомой многим под именем MIDAS); как создавать собственные серверные объекты для ASP-приложений; Ж как использовать службы компонентов Microsoft. Некоторые из вошедших в эту книгу материалов были ранее опубликованы нами в журнале «Компьютер Пресс» и в дальнейшем переработаны с учетом из- менений, произошедших в связи с выходом новых версий продуктов и техноло- гий, обсуждаемых в данной книге.
16 Введение Большая часть глав книги построена по принципу «от простого к сложному» — начинаются они с краткого экскурса в соответствующий раздел СОМ-техноло- гии и простейших примеров, доступных даже не очень опытным программистам. Затем материал постепенно усложняется, и в конце главы рассматриваются при- меры создания приложений с необычными или даже нестандартными возможно- стями. Мы надеемся, что приведенные в этой книге примеры использования Delphi и СОМ-техпологии помогут вам, уважаемые читатели, ответить на некоторые ваши вопросы или взглянуть па них с иной точки зрения и, может быть, найти нестандартные решения стоящих перед вами проблем. Возможно, какие-то из примеров приведут вас к новым идеям или дадут повод для творчества, которое сейчас необходимо любому хорошему программисту. Для кого написана эта книга Для кого предназначена эта книга? Главным образом для тех пользователей Delphi, которые уже неплохо знакомы со средой разработки, языком программирования и использованием баз данных и которые имеют определенный практический опыт создания приложений с помощью этого средства разработки. В данной книге темы, связанные с применением среды разработки, изучением языка програм- мирования и принципов создания приложений, использующих базы данных, не рассматриваются, а интересующиеся этими вопросами могут обратиться к докумен- тации, поставляемой с Delphi, или к другим книгам, посвященным этому про- дукту — их выбор сейчас достаточно велик. Пользователи C++Builder также смогут найти в этой книге для себя кое-что полезное. Что находится на компакт-диске К книге прилагается компакт-диск, па котором вы найдете: архив с исходными текстами примеров, описанных в книге; ознакомительную версию Delphi 7. Подробную инструкцию по использованию находящихся па компакт-диске примеров вы найдете в приложении. От издательства Ваши замечания, предложения, вопросы вы можете отправить по адресу элек- тронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мление. Все исходные тексты, приведенные в книге, вы можете найти по адресу http:// wwv/ piter.com/download. На web-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
ГЛАВА 1 Основы технологии СОМ COM (Component Object Model — модель многокомпонентных объектов) — одна из базовых технологий Windows. Более того, все новые технологии Windows (Shell, Scripting, HTML и т. п.) реализуют свои прикладные программные интер- фейсы (Application Program Interface, API) именно в виде СОМ-интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели СОМ и умения с ней работать. В этой главе мы рассмотрим основные понятия СОМ и особенности их под- держки в Delphi. Цели и задачи технологии СОМ Основная цель технологии СОМ — обеспечение возможности экспорта объектов. Идея экспорта объектов заключается в том, что один модуль создает объект, а дру- гой его использует посредством обращения к методам или сервисам. Конечно, в экспорте простейших объектов обычно не возникает необходимости — любой программист может создавать объекты с заданными свойствами в рамках своего приложения — определяющим при этом является время, требующееся для напи- сания кода. Однако предположим, что где-то и кем-то был реализован достаточно сложный алгоритм распознавания текста в файле формата BMP, получаемом при сканировании документов. Конечно же, производители сканеров захотят предо- ставить дополнительные возможности покупателям и пожелают включить такое программное обеспечение в свой пакет. При этом любая фирма будет стараться свести к минимуму число приложений в своем пакете: по возможности все сервисы должны предоставляться одним приложением. На первый взгляд, эта проблема решается достаточно просто, по крайней мере, когда в качестве другого модуля используется динамически загружаемая библиотека (Dynamic Link Library, DLL). В этом случае оба модуля содержатся в одном и том же адресном пространстве. Казалось бы, достаточно создать в DLL объект, а его методы вызвать из основного приложения — и задача решена. Чтобы попять, что это не так, рассмотрим небольшой пример. Определим метод IsFont в приложении: procedure IsFontCO: TObject): begi n if 0 is TFont then ShowMessage('Font')
18 Глава 1. Основы технологии СОМ el se ShowMessagel'Not font'); end; procedure TForml.ButtonlClick(Sender: TObject); begin . IsFont(Font); end; Вызовем этот метод из приложения. В качестве параметра метода Is Font ис- пользуем свойство Font формы. Как и ожидалось, появится сообщение, что объ- ект является шрифтом. Теперь создадим динамически загружаемую библиотеку, поместим в нее функ- цию IsFont и объявим ее экспортируемой: library FontLib; uses SysUtils, Dialogs. Graphics, Classes: procedure IsFontCO: TObject); begin if 0 is TFont then ShowMessagel'Font') else ShowMessagel'Not font'); end; exports IsFont name 'IsFont'; begin end. Пусть функция IsFont вызывается из библиотеки при щелчке на кнопке Button2 в вызывающем приложении: procedure IsF(O-.TObject): external 'FontLib.dll' name 'IsFont'; procedure TForml.Button2Click(Sender: TObject); begin IsF(Font); end: Результат этого действия окажется противоположным — теперь мы получим сообщение о том, что данный объект не является шрифтом. Таким образом, при
Цели и задачи технологии СОМ 19 создании приложения, состоящего из нескольких двоичных модулей, некоторые традиционные приемы уже не работают. В частности, оператор i s всегда возвра- тит False, а использование оператора as приведет к исключению. Вторая проблема возникает при обобщении первой проблемы. В приведен- ном выше примере было известно заранее, какую функцию надо вызвать и каков список ее параметров. Соответственно, в приложении перед компиляцией про- граммистом помещается строка кода: procedure IsF(O: TObject); external 'FontLib.dll' name ’IsFont’; Эта строка определяет имя функции IsFont и список ее параметров 0: TObject, поэтому программисту, использующему эту библиотеку, потребуется документа- ция — список функций со списком их формальных параметров. Их реализация «вручную» с большой вероятностью приведет к ошибкам. Для исправления оши- бок потребуется время, причем без всякой гарантии, что все ошибки будут обна- ружены. Хотелось бы, чтобы сам модуль мог информировать среду разработки о том, какие методы ей доступны и каковы списки их формальных параметров. Сложности работы с различными модулями этим не ограничиваются. Напри- мер, если в одном из модулей зарезервирована память для хранения данных, то в другом модуле без специальных мер нельзя ни освободить ее, пи изменить ее размер. Это связано с тем, что в каждом модуле имеется собственный менеджер памяти. Если один модуль выделяет память, то в менеджере памяти другого модуля это никак не фиксируется. Для реализации операций, связанных с применени- ем в различных модулях общих областей памяти, необходимо наличие общего менеджера памяти. Для его создания в Delphi используется модуль ShareMem. Еще одна проблема возникает при обращении к объекту, созданному другим приложением. В этом случае указатель па объект в памяти, созданный в одном приложении, является недействительным для другого приложения. Если пере- дать указатель из одного приложения в другое, последнее будет обращаться со- всем не к тем ячейкам оперативной памяти компьютера, в которых реально нахо- дятся данные, а это приведет к некорректному функционированию программы. Проблему можно представить глобально, если рассмотреть возможность созда- ния объекта па одном из компьютеров, а использования его через сеть на другом. Очередная проблема возникает при передаче двоичных данных от одного приложения к другому. Многие языки программирования имеют разное внут- реннее представление переменных (например, строки, логические значения), и при получении данных от заранее неизвестного приложения их интерпретация подчас невозможна. Можно выделить и другие проблемы, которые встречаются при традицион- ном программировании, например: К поиск установленной копии приложения, реализующего требуемые сервисы, и корректная его инициализация; обеспечение корректной работы приложения-сервера одновременно с несколь- кими клиентами; управление памятью, выгрузка из памяти приложения-сервера, когда необхо- димость в нем отпадет и, наоборот, предотвращение несвоевременной выгрузки.
20 Глава 1. Основы технологии СОМ Все эти проблемы решаются с помощью технологии СОМ. Проблемы вызова методов объектов, освобождения и резервирование памяти решаются с помощью интерфейсов. Проблема предоставления среде разработки информации о названиях методов объектов и списков формальных параметров решается при помощи библиотек типов. Проблема передачи данных из адресного пространства одного приложения в адресное пространство другого приложения и унифицированного представления данных решается путем маршалинга. И наконец, проблему авто- матического запуска сервера решает использование фабрики классов и ее реги- страции в системном реестре. Ниже приведены более детальные описания мето- дов решения вышеперечисленных проблем. Базовые понятия Интерфейс Основным понятием, на котором основана модель СОМ, является понятие ин- терфейса (interface). Без четкого понимания того, что такое интерфейс, невоз- можно успешное программирование СОМ-объектов. Интерфейс является контрактом между программистом и компилятором: Я программист обязуется реализовать все методы, описанные в интерфейсе, и следовать требованиям па реализацию некоторых их них; компилятор обязуется создать в программе внутренние структуры, позволяю- щие обращаться к методам этого интерфейса из любого поддерживающего те же соглашения средства программирования. Таким образом, СОМ является языково-независимой технологией и может использоваться в качестве «клея», соединяющего программы, написанные па разных языках. Объявление интерфейса включает в себя описание методов и их параметров, но не включает реализации методов. Кроме этого, в объявлении может указы- ваться идентификатор GUID интерфейса — уникальное 16-байтовое число, сге- нерированное по специальным правилам, гарантирующим его статистическую уникальность; о GUID мы расскажем чуть позже. Интерфейсы могут наследоваться. При наследовании подразумевается, что унаследованный интерфейс должен включать в себя все методы предка. Таким образом, необходимо попять следующее. И Интерфейс — это не класс. Класс может выступать в роли реализации интер- фейса, но класс содержит код методов па конкретном языке программирова- ния, а интерфейс — нет. S Интерфейс строго типизирован. Как клиент, так и реализация интерфейса должны использовать именно те методы и их параметры, которые указаны в описании интерфейса. Интерфейс является неизменным контрактом. Не следует определять новую версию того же интерфейса с измененным набором методов (или их парамет- ров), но с тем же идентификатором. Это гарантирует, что новые интерфейсы
Базовые понятия 21 никогда не будут конфликтовать со старыми. Если возникнет необходимость в расширении функциональности, следует определить новый интерфейс, воз- можно, являющийся наследником старого, и реализовать дополнительные методы в нем. Реализация интерфейса — это непосредственно код, реализующий методы интерфейса. При этом, за несколькими исключениями, не накладывается ника- ких ограничений на то, каким образом будет выглядеть реализация. Физиче- ски реализация представляет собой массив указателей па методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация ин- терфейса содержит метод Query interface, позволяющий запросить ссылку на кон- кретный интерфейс из числа реализуемых. Идентификатор GU1D Теперь перейдем к рассмотрению проблемы выбора необходимого интерфейса из того многообразия, которое представлено в иерархии интерфейсов. Имеется существенное расхождение между идентификацией класса и интерфейса. Эти различия связаны с тем, что классы используются внутри одного и того же модуля, а интерфейсы — в различных модулях. Для того чтобы создать класс с заданными свойствами, его имя просто указывается перед конструктором. Программист сам следит за тем, чтобы имена различающихся классов не совпадали, а при их совпадении — чтобы вызывался конструктор нужного класса. При работе с не- сколькими модулями такой подход невозможен, поскольку модули могут созда- ваться разными разработчиками в разное время. Если бы интерфейсы различались только по именам, то при случайном совпа- дении имен (а это происходило бы довольно часто: имя обычно песет в себе смысловую нагрузку) двух интерфейсов, реализованных в разных модулях, вместо одного модуля загружался бы другой. Поэтому для идентификации интерфейса используется структура типа GUID (Globally Unique Identifier — глобально уни- кальный идентификатор), которая имеет размер 16 байт (128 бит). Единствен- ный тип данных, который предопределен для интерфейса, — это GUID. Каждый СОМ-иптерфейс содержит собственный идентификатор GUID. Если разработ- чик реализует новый интерфейс, то этот интерфейс обязательно должен иметь GUID, причем уникальность должна соблюдаться не только в рамках данного компьютера разработчика, но и для всего мира. Эта глобальная уникальность достигается путем генерации GUID как псевдослучайного числа по алгоритму, определенному консорциумом Open Systems Foundation (сейчас он носит назва- ние Open Group) и использующему некоторые специфические характеристики данного компьютера (такие, как МАС-адрес сетевого адаптера). Алгоритм гаран- тирует, что все сгенерированные новые значения GUID будут отличаться от по- лученных ранее, даже если они генерируются на разных компьютерах. Описанная выше структура интерфейса естественно решает первую проблему экспорта объектов между модулями — каждый интерфейс однозначно определя- ется своим идентификатором GUID, имеет вполне определенный список мето- дов с вполне определенными параметрами и условиями вызова. Таким образом, имеется однозначный протокол вызова методов, и это гарантирует корректность передаваемых данных и сохранность стека.
22 Глава 1. Основы технологии СОМ Автоматическое управление памятью и подсчет ссылок Кроме предоставления независимого от языка программирования доступа к ме- тодам объектов, СОМ реализует автоматическое управление памятью для СОМ- объектов. Это управление основано па идее подсчета ссылок па объект. Любой клиент, которому требуется использовать COM-объект после его создания, дол- жен вызвать заранее предопределенный метод, увеличивающий на единицу внут- ренний счетчик ссылок на объект. По завершении использования объекта клиент вызывает другой его метод, уменьшающий значение того же счетчика. При дости- жении счетчиком ссылок пулевого значения COM-объект автоматически удаляет себя из памяти. Такая модель позволяет клиентам не вдаваться в подробности реализации объекта, а объекту — обслуживать несколько клиентов и корректно очистить память по завершении работы с последним из них. Объявление интерфейсов Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополни- тельными ключевыми словами. Объявление интерфейса в Delphi реализуется с помощью ключевого слова interface: type IMylnterface = interface ['{412AFF00-5C21-11D4-84DD-C8393F763A13}'] procedure DoSomething(var I: Integer); stdcall; function DoSomethingAnother(S: String): Boolean; end; IMyInterface2 = interface!IMylnterface) [’{412AFF01-5C21-11D4-84DD-C8393F763A13}’] procedure DoAdditional (var I: Integer); stdcall; end; Для генерации нового значения GUID в среде разработки Delphi используется сочетание клавиш Ctrl+Shift+G. Интерфейс IUnknown Базовым интерфейсом в модели СОМ является интерфейс IUnknown. Любой дру- гой интерфейс наследуется от IUnknown и должен реализовать объявленные в нем методы. Интерфейс IUnknown объявлен в модуле System.pas следующим образом: type IInterface = interface [{00000000-0000-0000-С000-000000000046}'] function Querylnterface(const IID: TGUID; out Obj ): HResult: stdcall: function _AddRef; Integer; stdcall: function _Release: Integer; stdcall; end; IUnknown = I Interface;
Базовые понятия 23 ВНИМАНИЕ ---------------------------------------------------------- В связи с поддержкой мультиплатформеппой разработки (Kylix) в последних версиях Delphi базовым интерфейсом является интерфейс I Interface, описание которого пол- ностью совпадает с описанием интерфейса IUnknown. Поскольку книга посвящена тех- нологиям СОМ, в дальнейшем в качестве базового интерфейса мы будем рассматри- вать интерфейс IUnknown. На уровне интерфейса IUnknown, который является предком для всех интерфей- сов, уже реализовано корректное решение проблемы резервирования и освобож- дения ресурсов операционной системы. Напомним эту проблему: если в каком- нибудь модуле были затребованы ресурсы операционной системы, то их нельзя возвратить системе в другом модуле. Для начала вспомним, каким образом происходит резервирование и возвра- щение системных ресурсов при работе с классами. При создании экземпляра класса вызывается его конструктор, который получает у системы все необходи- мые ему ресурсы. После того как рабочая копия становится ненужной, вызы- вается деструктор объекта. При этом если все реализовано корректно, ресурсы вновь возвращаются системе. Методы интерфейса, как будет показано ниже, также реализуются в классах. Однако описанную выше методику применить в этом случае невозможно. Если в интерфейсе будет определен метод Destroy, который непосредственно обраща- ется к деструктору, то при попытке вызвать его из модуля, где была получена ссылка на интерфейс, произойдет исключение. Это связано с тем, что различные модули, даже реализованные па одном языке программирования, имеют собст- венные менеджеры памяти, и освобождение ресурсов должно происходить в рам- ках того модуля, в котором они были востребованы. Также следует принять во внимание, что один и тот же интерфейс может быть затребован одновременно несколькими клиентами. Например, пусть два клиентских приложения одновременно работают с Microsoft Word. Если произ- водить освобождение ресурсов интерфейса тем же способом, что и освобождение ресурсов класса, то первое клиентское приложение просто закроет Word. Вто- рое, в общем случае, не будет уведомлено об этом, поэтому оно сохранит ссылку па несуществующий объект. Нетрудно догадаться, что произойдет в этом случае при попытке вызвать методы этого объекта. Для решения подобных проблем в интерфейсах при их реализации осуществ- ляется подсчет ссылок. Специально для подсчета ссылок во всех интерфейсах имеются методы _AddRef и _Rel ease. Метод _AddRef при реализации обязан увели- чивать счетчик внутренней переменной. «Обязан» потому, что реализация мето- дов интерфейсов возложена на разработчика, который создает COM-сервер. Он может этого и не сделать, но тогда интерфейс будет работать некорректно. Соот- ветственно, метод _Rel ease обязан уменьшать этот счетчик. Кроме того, этот метод должен проверять, равен ли счетчик ссылок пулю, и, если равен, вызывать дест- руктор объекта, в котором реализован интерфейс. Теперь все становится на свои места. Когда клиент требует ссылку па интер- фейс, клиентом в адресном пространстве сервера создается объект, резервируются
24 Глава 1. Основы технологии СОМ ресурсы и вызывается метод AddRef. Клиент напрямую не вызывает этот метод — он вызывается при выполнении метода Query Interface, о нем речь — чуть ниже. Если другой клиент затребует тот же самый интерфейс, то, чаще всего, увеличи- вается счетчик ссылок па уже имеющийся объект, и указатель передается второму клиенту (допустима ситуация, когда создается вторая копия объекта в памяти, но счетчик ссылок в обеих копиях остается равным единице). Соответственно, когда клиенту уже не нужен интерфейс, он вызывает метод _Rel ease. Этот метод уменьшает счетчик ссылок на единицу. Одновременно проверяется, равен ли счетчик ссылок пулю, и, если равен, вызывается деструктор объекта. Вызов деструктора происходит из сервера — поэтому ресурсы освобождаются корректно. В вышеприведенном примере счетчик ссылок становится равным нулю при вызове метода _Release обоими клиентами. Интерфейс IUnknown содержит также метод Queryinterface, который исполь- зуется для получения клиентом ссылок па интерфейс. Клиент вызывает метод Queryinterface интерфейса IUnknown сервера и указывает идентификатор IID (Interface Identifier — идентификатор интерфейса) того интерфейса, ссылку па который он хочет получить (IID — это тот же самый тип данных, что и GUID, и применяется он для идентификации интерфейсов). Как получить ссылку па интерфейс IUnknown сервера, будет рассмотрено позднее при описании интер- фейса IClassFactory. Метод Query Interface обязан проверять все идентификаторы IID интерфейсов, реализованные в данном классе многокомпонентного объекта (CoClass, COM-класс). Если будет найден совпадающий идентификатор IID, то метод Queryinterface обязан сделать следующее. 1. Вызвать конструктор, если не был создан экземпляр класса, в котором реали- зован интерфейс. 2. Вызвать метод _AddRef для затребованного интерфейса и тем самым увели- чить счетчик ссылок па 1. Иногда для группы интерфейсов реализуется об- щий счетчик ссылок. 3. Поместить указатель па созданный (или имеющийся) объект, в котором реа- лизован интерфейс, в петипизированпую переменную Р. 4. Возвратить результат S OK. Если же интерфейс с данным идентификатором IID не поддерживается, то в иетипизированиую переменную Р возвращается ni 1, и результат вызова ме- тода должен быть равен E NOINTERFACE. Типичный пример реализации метода Queryinterface приведен ниже: function TMyClassFactory.Querylnterfacelconst lid: TIID; van P): HResult: begin if IsEqualIID(iid, IID_IClassFactory) or IsEqualIID(iid, IID_IUnknown) then begin Pointer(P) := Self: _AddRef: Result := S OK;
Базовые понятия 25 end else begin Pointer(P) := nil; Result := E_NOINTERFACE: end: end: В принципе, в конкретной реализации методы интерфейса IUnknown могут обес- печивать и какую-либо другую функциональность, отличающуюся от стандарт- ной, однако делать этого не рекомендуется, поскольку в этом случае интерфейс будет несовместим с моделью СОМ. В модуле System.pas объявлен класс TInterfacedObject, реализующий интер- фейс IUnknown и методы этого интерфейса. Рекомендуется использовать этот класс для реализаций своих интерфейсов. Кроме этого, поддержка интерфейсов реализована в базовом классе TObject. Он имеет следующий метод: function TObject.Getlnterfacelconst IID: TGUID: out Obj): Boolean; Если класс реализует запрошенный интерфейс, то эта функция: « возвращает ссылку на этот интерфейс в параметре Obj; М вызывает метод _AddRef полученного интерфейса; » возвращает True. В противном случае функция возвращает False. Таким образом, имеется возможность запросить у любого класса Delphi реа- лизуемый им интерфейс. Подробнее использование этой функции будет рассмот- рено ниже. Реализация интерфейсов — основные сведения Реализация интерфейса в Delphi всегда осуществляется в классе. Для этого в объяв- лении класса необходимо указать, какие интерфейсы он реализует. type TMyClass = class(TComponent. IMylnterface. IDropTarget) // Реализация методов end; Класс TMyClass реализует интерфейсы IMylnterface и IDropTarget. Необходимо понимать, что реализация классом нескольких интерфейсов не означает множе- ственного наследования и вообще наследования класса от интерфейса. Указание интерфейсов в описании класса означает лишь то, что в данном классе реализо- ваны все эти интерфейсы. Класс должен иметь методы, точно соответствующие по именам и спискам параметров всем методам всех объявленных в его заголовке интерфейсов. Воз- можно отображение на методы интерфейса методов с другими именами. О том, как это сделать, рассказано ниже.
26 Глава 1. Основы технологии СОМ Рассмотрим более подробный пример: type ITest = interface ['{61F26D40-5СЕ9-11D4-84DD-F1B8E3A70313}'] procedure Beep; end: ITest = classdlnterfacedObject. ITest) procedure Beep: destructor Destroy: override: end: procedure TTest.Beep: begin Windows.Beep(O.O): end: destructor TTest.Destroy: begi n inherited: MessageBox(0. 'TTest.Destroy', nil. 0): end: Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование ин- терфейса из программы: procedure TForml.ButtonlClick(Sender: TObject); var Test: ITest; begin Test := TTest.Create; Test.Beep; end: Данный код выглядит довольно странно, поэтому остановимся на нем под- робнее. Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод AddRef. При этом количество ссылок па интерфейс увели- чивается па 1. Во-вторых, в коде не делается никаких попыток освободить память, выделен- ную под объект TTest. Тем не менее, если выполнить эту программу, то па экран будет выведено сообщение о том, что вызывался деструктор. Это происходит по- тому, что при выходе переменной, ссылающейся на интерфейс, за область види- мости (либо при присвоении ей другого значения) компилятор Delphi генерирует
Базовые понятия 27 код для вызова метода Release, информируя реализацию о том, что ссылка на нее больше не нужна. ВНИМАНИЕ --------------------------------------------------------- Если у класса запрошен хотя бы один интерфейс — не вызывайте его метод Free (или Destroy). Класс будет освобожден тогда, когда отпадет необходимость в последней ссылке на его интерфейсы. Если вы к этому моменту уничтожите экземпляр класса вручную — произойдет ошибка доступа к памяти. Так, следующий код приведет к ошибке в момент выхода из функции: var Test: ITest; Т: TTest: begin T := TTest.Create; Test := T: Test.Beep: T.Free: end: // в этот момент произойдет ошибка Если вы хотите уничтожить реализацию интерфейса немедленно, не дожи- даясь выхода переменной за область видимости, — просто присвойте ей значе- ние nil: var Test: ITest; T: TTest: begin T := TTest.Create; Test := T: Test.Beep; Test := nil; // Неявно вызываются I Unknown._Release end; Обратите особое внимание на то, что вызовы методов интерфейса IUnknown осуществляются Delphi неявно и автоматически. Поэтому не следует вызывать методы IUnknown самостоятельно. Это может нарушить нормальный подсчет ссы- лок и привести либо к нарушениям защиты памяти при работе с интерфейсами, либо к тому, что память не будет освобождена. Во избежание этого необходимо просто помнить, что: й при приведении типа объекта к интерфейсу вызывается метод _AddRef; при выходе переменной, ссылающейся на интерфейс, за область видимости, а также при присвоении ей другого значения вызывается метод _Release; Ж единожды запросив у объекта интерфейс, в дальнейшем не следует освобождать объект вручную (лучше вообще, начиная с этого момента, работать с объек- том только через интерфейсные ссылки).
28 Глава 1. Основы технологии СОМ В рассмотренных примерах код для получения интерфейса у класса генери- ровался (с проверкой типов) па этапе компиляции. Если класс не реализует тре- буемого интерфейса, то программа не компилируется. Однако существует воз- можность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает метод Queryinterface и, в случае успеха, возвращает ссылку па полученный интерфейс. В противном случае генерируется исключение. Например, следующий код будет успешно откомпилирован, по при выполне- нии вызовет ошибку Interface not supported (интерфейс не поддерживается): var Test: ITest: begin Test := TInterfacedObject.Create as ITest: Test.Beep: end; В то же время представленный ниже код будет успешно компилироваться и выполняться: var Test: ITest: begin Test := TTest.Create as ITest; Test.Beep; end; Для лучшего понимания интерфейсов сравним их с классами, сведения о кото- рых известны из объектно-ориентированного программирования. Для интерфей- сов, так же как и для классов, определено понятие иерархии. Суть этого понятия: каждый класс (или интерфейс) может иметь одного или нескольких потомков. Во всех потомках данного класса (интерфейса) сохраняются все методы (и данные для классов), которые имеются у родителей. В Delphi классы (точно так же, как и интерфейсы) могут иметь только одного родителя, поэтому они образуют иерар- хическое дерево. Его можно увидеть в Delphi при выборе команды View ► Browser в меню среды разработки. В вершине иерархического дерева классов находится родоначальник всех классов — TObject, а интерфейсов — IUnknown (рис. 1.1). Все остальные потомки содержат методы TObject (IUnknown). В классах Delphi различают три типа методов — статические, динамические и виртуальные. При генерации компилятором кода для статического метода класса указывается его относительный адрес в памяти, и при вызове такого ме- тода управление просто передается по указанному адресу. Соответственно, одна копия такого метода в памяти обслуживает все экземпляры данного класса и его потомков. При вызове же динамических и виртуальных методов процесс обраща- ется к таблицам виртуальных или динамических методов. Эти таблицы хранятся в ОЗУ компьютера, и уже оттуда извлекаются адреса вызываемых методов. Далее управление передается по найденному адресу. Обсуждение различий между таб-
Базовые понятия 29 лицами виртуальных и динамических методов выходит за рамки данной книги. Интересующимся этим вопросом мы рекомендуем книгу Рэя Лишпера (Ray Lishner, Secrets of Delphi 2: Exposing Undocumented Features of Delphi, Waite Group Press, 1996). Хотя она и выпущена довольно давно, но по-прежнему является од- ним из лучших источников информации по данному вопросу. Рис. 1.1. Иерархия интерфейсов в браузере объектов Delphi Абстрактным методом называется виртуальный или динамический метод, ко- торый объявлен, по не реализован. Классы, в которых имеются абстрактные ме- тоды, называются абстрактными классами. При объявлении абстрактного метода в таблицу виртуальных (динамических) методов заносится значение nil. При попытке вызвать абстрактный метод из экземпляра такого класса генерируется исключение EAbstractError. Абстрактные методы требуют их перекрытия в потомках данного класса. Например, пусть имеется абстрактный класс TStream, в котором определены два абстрактных метода — Read и Write. В потомках этого класса — TMemoryStream и TFil eSt ream — эти методы реализованы так, что происходит чтение (запись) дан- ных в память для первого класса и в файл — для второго. Поэтому достаточно написать всего одну процедуру для сохранения данных в поток данных (stream), при этом параметром этого метода является объект тина TStream. Далее, при вызове этой процедуры с объектом TMemoryStream все данные запишутся в память, а с объектом TFileStream — в файл. Все методы интерфейсов являются виртуальными и абстрактными. Иными словами, они только объявлены, по не реализованы. Возникает вполне естест- венный вопрос — а зачем это надо? Для ответа необходимо вспомнить, что ин- терфейс создается в одном модуле (в основном па COM-сервере), а используется в другом (в COM-клиенте). Для того чтобы клиент знал, какие методы имеют- ся в данном интерфейсе и какие параметры необходимо указать при вызове данного метода, используются абстрактные методы. Получив ссылку па создан- ный сервером интерфейс, клиент знает, например, что имеется метод (функция) Query Interface с первым параметром типа TGUID и со вторым в виде петипизиро- ваппой переменной, причем после выполнения метод возвращает переменную
30 Глава 1. Основы технологии СОМ типа HResult. Соответственно, клиент может вызвать этот метод с данным спи- ском параметров, и сервер обязан его выполнить. Следует обратить внимание на то, что главным для интерфейса является не название метода (Queryinterface), а список параметров данного метода и то, каким но очереди он был объявлен. Метод Queryinterface объявляется в интер- фейсе IUnknown третьим по счету (первые два метода этого интерфейса — _AddRef и _Release). Легко себе представить язык программирования, в котором интер- фейс IUnknown может быть объявлен, например, следующим образом: IUnknown=i nterface functi on AddRef;i nteger; safecal1; function Release integer; safecall; function Getlnterf(const IID:TGUID; var P):HResult: safecall; end; To есть вместо метода Queryinterface объявлен метод Getlnterf. При написа- нии кода па таком абстрактном языке программирования придется вводить слово «Getlnterf» вместо «Queryinterface», но полученный код останется работоспо- собным! Наоборот, приведенное ниже объявление интерфейса IUnknown не будет работать ни в одном из языков программирования: IUnknown=i nterface function AddRef:integer; safecall; function Querylnterfacelconst IIDTGUID; var P):HResult: safecall; function Release integer: safecall; end: Этот код отличается от объявления интерфейса IUnknown в Delphi тем, что ме- тоды Query Interface и Release поменялись местами. Порядок объявления методов определяет место методов в виртуальной таблице. Таким образом, вызов методов интерфейса осуществляется следующим обра- зом. Интерфейс создается на сервере, а клиент получает иа него ссылку (как это делается — будет рассказано ниже). После этого клиент для вызова метода Query Interface производит следующие операции. 1. В стек помещаются адрес, где находится переменная HResult, и адреса пере- менных Р и IID. 2. На основании полученной ссылки па интерфейс вычисляется адрес таблицы виртуальных методов. 3. Находится третий столбец данной таблицы (поскольку метод Queryinterface реализован третьим). 4. Из таблицы виртуальных методов извлекается адрес метода и ему передается управление. 5. После возвращения управления очищается стек. Из этой схемы вызова методов интерфейса можно сделать вывод, что все языки, поддерживающие технологию СОМ, обязаны создавать таблицу виртуаль- ных методов для COM-объектов. Эта таблица везде имеет одинаковый размер
Базовые понятия 31 записи и относительный адрес в COM-объекте. Такая жесткость требований необходима для того, чтобы приложения, написанные на разных языках про- граммирования, могли взаимодействовать друг с другом. При этом каждый из языков программирования может иметь собственные — как, например, таблица динамических методов в Delphi. Реализация интерфейсов — расширенное рассмотрение Рассмотрим вопросы реализации интерфейсов подробнее. Объявим два интерфейса: type ITest = interface [’{61F26D40-5СЕ9-11D4-84DD-F1B8E3A70313}'] procedure Beep: end: ITest2 = interface ['{61F26D42-5CE9-1104-84DD-F1B8E3A70313}' ] procedure Beep: end; Теперь создадим класс, который будет реализовывать оба этих интерфейса: TTest2 = cl ass(TInterfacedObject, ITest. ITest2) procedure Beepl; procedure Beep2: procedure ITest.Beep = Beepl; procedure ITest2.Beep = Beep2: end: Как видно, класс не может содержать сразу два метода Веер. Поэтому Delphi предоставляет способ для разрешения конфликтов имен, позволяя явно указать, ка- кой метод класса будет служить реализацией соответствующего метода интерфейса. Если реализации методов TTest2. Beepl и TTest2. Веер2 идентичны, то можно не создавать двух разных методов, а объявить класс следующим образом: TTest2 = class(TInterfacedObject. ITest. ITest2) procedure MyBeep; procedure ITest.Beep = MyBeep; procedure ITest2.Beep = MyBeep; end; При реализации классов, поддерживающих множество интерфейсов и мето- дов, может оказаться удобным делегировать реализацию некоторых их них до- черним классам. Рассмотрим пример класса, реализующего два интерфейса: type ТВеерег = class procedure Веер; end;
32 Глава 1. Основы технологии СОМ TMessager = class procedure ShowMessage(const S: String); end; TTest3 = class(TInterfacedObject. ITest. lAnotherTest) private FBeeper: TBeeper; FMessager: TMessager property Beeper: TBeeper read FBeeper implements ITest: property Messager: TMessager read FMessager implements lAnotherTest; public constructor Create: destructor Destroy: override: end: Для делегирования реализации интерфейса другому классу служит ключевое слово implements. { TBeeper } procedure TBeeper.Веер: begi n Windows.Beep(0,0); end: { TMessager } procedure TMessager.ShowMessage(const S: String); begin MessageBox(0, PChar(S), nil, 0); end; { TTest3 } constructor TTest3.Create; begin inherited; // Создаем экземпляры дочерних классов FBeeper .- = TBeeper.Create: FMessager := TMessager.Create: end; destructor TTest3.Destroy; begin // Освобождаем экземпляры дочерних классов FBeeper.Free: FMessager.Free; inherited: end;
Базовые понятия 33 Такой подход позволяет разбить реализацию сложного класса па несколько простых, что упрощает программирование и повышает степень модульности про- граммы. Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы: var Test: ITest; Test2; lAnotherTest: begin Test2 := TTest3.Create: Test2.ShowMessage('Hi’): Test := Test2 as ITest: Test.Beep: end: Интерфейсы и класс TComponent В базовом классе TComponent библиотеки VCL (Visual Component Library — биб- лиотека визуальных компонентов) имеется полный набор методов, позволяющих реализовать интерфейс IUnknown, хотя сам класс не реализует этого интерфейса. Это позволяет наследникам класса TComponent реализовывать интерфейсы, не заботясь о реализации интерфейса IUnknown. Одпако методы TComponent. AddRef и TComponent._Release па этапе выполнения программы не предоставляют меха- низма подсчета ссылок, и, соответственно, для классов-наследников TComponent, реализующих интерфейсы, не действует автоматическое управление памятью. Это позволяет запрашивать у них интерфейсы, не опасаясь, что объект будет удален из памяти при выходе переменной за область видимости. Таким образом, следую- щий код совершенно корректен и безопасен: type IGetData = interface ['{B5266AE0-5E77-11D4-84DD-9153115ABFC3}'] function GetData: String; end; TForml = classCTForm, IGetData) private function GetData: String; end: var I: Integer; GD: IGetData; S: String: begin
34 Глава 1. Основы технологии СОМ S := "; for I := 0 to PrecKScreen.FormCount) do begin if Screen.Forms[I].GetInterface(IGetData. GD) then S := S + GD.GetData + #13; end; ShowMessage(S); end; Этот код проверяет наличие у всех форм в приложении реализации интер- фейса IGetData и, если форма реализует этот интерфейс, вызывает его метод. Использование интерфейсов внутри программы Рассмотренное выше поведение класса TComponent позволяет просто, не теряя строгой типизации, связывать компоненты приложения и единообразно вызы- вать их методы, не требуя, чтобы компоненты были унаследованы от общего предка. Достаточно лишь реализовать в компоненте интерфейс, а в вызывающей про- грамме — проверить его наличие. Рассмотрим в качестве примера MDI-приложение (MDI, Multiply Document Interface — многодокументный интерфейс), имеющее много различных окон и еди- ную панель инструментов. Предположим, что на этой панели инструментов име- ются кнопки для команд Сохранить, Загрузить и Очистить, однако каждое из окоп реагирует на эти команды по-разному. Создадим модуль с объявлениями интерфейсов: unit Tool barInterface: interface type TCommandType = (ctSave, ctLoad, ctClear); TCommandTypes = set of TCommandType; TSaveType = (stSave. stSaveAS): IToolBarCommands = interface [' {B5266AE1-5E77-11D4-84DD-9153115ABFC3}'] function SupportedCommands: TCommandTypes; function Save(AType; TSaveType): Boolean; procedure Load; procedure Clear; end: implementation end. Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать окна, поддерживающие работу с панелью инструментов. Метод SupportedCommands возвращает список поддерживаемых формой (окном) команд.
Базовые понятия 35 Создадим три дочерние формы Form2, Form3 и Form4, установим их свойство FormStyle равным fsMDIChild. Форма Form2 умеет выполнять все три команды: type TForm2 = class(TForm, IToolBarCommands) private function SupportedCommands: TCommandTypes; function Save(AType: TSaveType): Boolean: procedure Load; procedure Clear: end; { TForm2 } procedure TForm2.Clear; begin ShowMessage('TForm2.Clear'); end; procedure TForm2.Load; begin ShowMessage(’TForm2.Load'): end: function TForm2.Save(AType: TSaveType): Boolean; begin ShowMessage(’TForm2.Save'); Result := True: end: function TForm2.SupportedCommands: TCommandTypes; begin Result := [ctSave. ctLoad. ctClear] end; Форма Form3 умеет выполнять только команду Очистить (метод Clear): type TForm3 = class(TForm. IToolBarCommands) private function SupportedCommands: TCommandTypes; function Save(AType: TSaveType): Boolean: procedure Load: procedure Clear: end: { TForm3 } procedure TForm3.Clear; begin
36 Глава 1. Основы технологии СОМ ShowMessaqe(’TForm3.Clear'): end: procedure TForm3.Load: begin // Метод ничего не делает, но должен присутствовать И для корректной реализации интерфейса end: function TForm3.Save(AType: TSaveType): Boolean; begin end; function TForm3.SupportedCommands: TCommandTypes: begin Result := [ctClear] end: И, наконец, форма Form4 вообще не реализует интерфейс IToolBarComniands и не умеет откликаться ни на одну из команд. На главной форме приложения поместим список ActionList и создадим три компонента TAction. Также разместим на ней панель инструментов TToolBar и на- значим ее кнопкам соответствующие объекты TAction. type TForml = class(TForm) Tool Bari: TToolBar: ImageListl: TlmageList; ActionListl: TActionList: acLoad: TAction: acSave: TAction: acClear: TAction; tbSave: TToolButton; tbLoad: TToolButton: tbClear: TToolButton: procedure acLoadExecute(Sender: TObject): procedure ActionListlUpdate(Action: TBasicAction: var Handled: Boolean): procedure acSaveExecute(Sender: TObject); procedure acClearExecute(Sender: TObject): end; Наиболее интересен метод ActionListlllpdate, в котором проверяются под- держиваемые активной формой команды и настраивается интерфейс главной формы. Если нет активной дочерней формы или форма не поддерживает интер- фейс ITool BarCommands, все команды запрещаются, в противном случае — разреша- ются только те команды, которые поддерживаются формой:
Базовые понятия 37 procedure TForml.ActionListlUpdate(Action: TBasicAction; var Handled: Boolean): var Supported: TCommandTypes: TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChiId.GetlnterfaceCIToolBarCommands. TC) then Supported := TC.SupportedCommands el se Supported := []: acSave.Enabled := ctSave in Supported: acLoad.Enabled := ctLoad in Supported: acClear.Enabled := ctClear in Supported: end: При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий метод этого интерфейса: procedure TForml.acLoadExecute(Sender: TObject): var TC: IToolBarCommands; begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetlnterfacedToolBarCommands, TC) then TC.Load: end: procedure TForml.acSaveExecute(Sender: TObject); var TC: IToolBarCommands: begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetlnterfacelIToolBarCommands, TC) then if not TC.Save(stSaveAS) then ShowMessage('Not Saved !!!'); end: procedure TForml.acClearExecuteCSender: TObject): var TC: IToolBarCommands: begin if Assigned(ActiveMDIChild) and ActiveMDIChild.GetlnterfacedToolBarCommands. TC) then TC.Clear: end:
38 Глава 1. Основы технологии СОМ Результат работы программы представлен па рис. 1.2. Рис. 1.2. Реализация интерфейсов поддержки команд Того же эффекта можно добиться и другими способами (например, унаследо- вав все дочерние формы от единого предка либо обмениваясь с ними сообще- ниями), однако эти способы имеют ряд существенных недостатков. Так, при обмене сообщениями мы теряем строгую типизацию и вынуждены передавать параметры через целые числа, а при визуальном наследовании мы привязываем себя к родительскому классу, что не всегда удобно. К тому же мож- но определить множество интерфейсов и реализовывать в каждой из дочерних форм лишь необходимые, а в случае с наследованием все формы должны будут реализовывать все общие методы. Использование интерфейсов для реализации модулей расширения Еще удобней использовать интерфейсы для реализации модулей расширения (plug-ins) программы. Как правило, такой модуль экспортирует ряд известных главной программе методов, которые могут быть вызваны из пего. В то же время часто ему нужно обращаться к каким-либо функциям вызывающей программы. То и другое удобно реализуется при помощи интерфейсов. В качестве примера напишем несложную программу, использующую модули расширения для загрузки данных. Объявим интерфейс модуля расширения и внутренний прикладной программ- ный интерфейс (API) программы: unit Plugininterface; interface
Базовые понятия 39 type IAPI = interface ['{64CFF1E0-61A3-11D4-84DD-B18D6F94141F}1] procedure ShowMessageCconst S: String): end: ILoadFilter = interface [’{64CFF1E1-61A3-UD4-84DD-B18D6F94141F}'] procedure Init(const FileName: String: API: IAPI); function GetNextLine(var S: String): Boolean: end: implementation end. Этот модуль должен задействоваться как в модуле расширения, так и в основ- ной программе, что гарантирует использование ими идентичных интерфейсов. Модуль расширения представляет собой динамическую загружаемую биб- лиотеку, экспортирующую функцию CreateFilter, которая возвращает ссылку па интерфейс ILoadFilter. Главный модуль должен сначала вызвать метод Init, пе- редав ему имя файла и ссылку на внутренний интерфейс API, а затем вызывать метод GetNextLine до тех пор, пока тот не вернет False. Рассмотрим код модуля расширения: library ImpTxt; uses ShareMem, Syslltils, Classes. Plugininterface: {$R *.RES} type TTextFilter = class(TInterfacedObject. ILoadFilter) private FAPI: IAPI: F: TextFile: Lines: Integer; InitSuccess: Boolean; procedure InitCconst FileName: String: API: IAPI): function GetNextLine(var S: String): Boolean; public destructor Destroy; override; end: { TTextFilter } procedure TTextFilter.InitCconst FileName: String: API: IAPI): begin FAPI -API:
40 Глава 1. Основы технологии СОМ {$!-} AssignFi1e(F, FileName); Reset(F); {$14 InitSuccess := lOResult = 0: if not InitSuccess then API.ShowMessageC'Ошибка инициализации загрузки'); end; Метод Init решает две задачи: сохраняет ссылку па API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл от- крыт успешно — выставляется внутренний флаг InitSuccess. function TTextFilter.GetNextLinelvar S; String): Boolean; begin if InitSuccess then begin Inc(Lines); Result := not Eof(F); if Result then begin Readln(F. S): F API. ShowMessageU Загружено ' + IntToStr(Lines) + ' строк.'); end; end else Result := FALSE; end; Метод GetNextLine считывает следующую строку данных и возвращает True, если это удалось, либо False, если достигнут конец файла. Кроме того, он при по- мощи интерфейса API, предоставляемого главным модулем, информирует поль- зователя о ходе загрузки. destructor TTextFilter.Destroy; begi n FAPI := nil; if InitSuccess then CloseFile(F); inherited; end: В деструкторе мы обнуляем ссылку па API главного модуля, уничтожая ин- терфейс, и закрываем файл. function CreateFilter: ILoadFilter; begin Result := TTextFilter.Create; end; Эта функция создает экземпляр класса, реализующего интерфейс ILoadFilter. Ссылок па экземпляр сохраггять не нужно, он будет освобожден автоматически.
Базовые понятия 41 exports CreateFi1 ter; И Функция должна быть экспортирована из DLL begin end. Теперь полученную динамически загружаемую библиотеку можно использо- вать в основной программе. type TAPI = class(TInterfacedObject, IAPI) procedure ShowMessage(const S: String); end; { TAPI } procedure TAPI.ShowMessage(const S: String); begin with (Application.MainForm as TForml).StatusBarl do begin SimpleText := S; Update: end; end; Класс TAPI реализует прикладной программный интерфейс, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в строку состояния главной формы приложения. type TCreateFilter = function: ILoadFilter; procedure TForml.LoadData(Fil eName: String); var PluginName: String; Ext: String; hPlugln: THandle: CreateFilter: TCreateFilter; Filter: ILpadFilter; S: String: begin Подготавливаем компонент TMemo к загрузке данных: Memol.Lines.Clear; Memol.Li nes.Begi nUpdate; Получаем имя модуля с фильтром для выбранного расширения файла. Описа- ния модулей хранятся в секции [Filters] файла plugins.ini в виде строк формата: <расширение> = <имя модуля>
42 Глава 1. Основы технологии СОМ Например: [Filters] ТХТ=ImpTXT.DLL try Ext := ExtractFileExt(FileName); DeleteCExt. 1, 1); with TIniFile.Create(ExtractFilePath(ParamStr(O)) + 'plugins.ini') do try PluglnName : = ReadString!'Filters’, Ext, ''); finally Free: end; Далее, пытаемся загрузить модуль и найти в нем функцию CreateFilter: hPlugln := LoadLibrary(PChar(PluginName)); try CreateFilter : = GetProcAddress(hPlugIn. 'CreateFilter'): if Assigned(CreateFilter) then begin Функция найдена, создаем экземпляр фильтра и инициализируем его. По- скольку внутренний API также реализован как СОМ-иитерфейс, нет необходи- мости сохранять ссылку на него: Filter := CreateFilter; try Filter.Init(FileName. TAPI.Create): Загружаем данные при помощи созданного фильтра: while Filter.GetNextLine(S) do Memol.Lines.Add(S) Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку па интерфейс модуля расширения, иначе это произойдет при выходе из функ- ции, что приведет к попытке выполнить неразрешенную операцию с памятью (Access Violation): finally Filter := nil: end: end else raise Exception.Create!'He могу загрузить фильтр'): Выгружаем DLL и обновляем поле ТМето: finally FreeLibrary(hPlugln): end:
Базовые понятия 43 finally Memol.Li nes.EndUpdate: end: end; Таким образом, несложно реализовывать модули расширения для загрузки данных в форматах, отличающихся от текстовых, и единообразно работать с ними. Достоинства данного способа становятся особенно очевидными при реализа- ции сложных модулей расширения, интерфейс с которыми состоит из множества методов. ВНИМАНИЕ ------------------------------------------------------------------ Поскольку в EXE- и DLL-файлах используются длинные строки, нс забудьте вклю- чить в секцию uses обоих проектов модуль Sha reMem. Другим вариантом решения про- блемы передачи строк является использование типа данных Wi deStri ng. Для данных этого типа распределением памяти занимаются средства поддержки СОМ, причем де- лают это независимо от модуля, с помощью которого была создана строка. СОМ-сервер Модель СОМ предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты на- зываются COM-серверами и представляют собой исполняемые файлы (EXE) или динамически загружаемые библиотеки (DLL), специальным образом оформ- ленные для обеспечения возможности их универсального вызова из любой про- граммы, написанной па поддерживающем СОМ языке программирования. При этом СОМ-сервер может выполняться как в адресном пространстве вызывающей программы {внутрипроцессный сервер — in-process server), так и в виде само- стоятельного процесса {внепроцессный сервер — out-of-process server) или даже на другом компьютере (в этом случае говорят о распределенной модели СОМ — Distributed СОМ, а сервер называют удаленным). СОМ автоматически разрешает вопросы, связанные с передачей параметров {маршалингом — marshalling) и со- гласованием моделей потоков клиента и сервера. СОМ-сервер — это специальным образом оформленное и зарегистрирован- ное приложение, которое позволяет клиентам создавать реализованные на сервере объекты. Сервер в виде DLL Сервер в виде DLL всегда выполняется в адресном пространстве активизировав- шего его приложения, то есть является внутрипроцессным сервером. За счет этого, как правило, снижаются накладные расходы на вызов методов сервера. В то же время такой сервер менее надежен, поскольку его память не защищена от ошибок в вызывающем приложении. Кроме того, он не может выполняться на удаленной машине без приложения-посредника, создающего процесс, в который может быть загружена библиотека сервера. Примерами таких приложений могут служить службы компонентов (СОМ+) и Internet Information Services.
44 Глава 1. Основы технологии СОМ Сервер в виде исполняемого файла Сервер в виде исполняемого файла представляет собой обычный исполняемый файл Windows, в котором реализована возможность создания COM-объектов по запросу других приложений. Примерами таких серверов являются приложения Microsoft Office. Интерфейс ICIassFactory и использование системного реестра Напомним, что в предшественнице OLE (Object Linking and Embedding — связы- вание и внедрение объектов) — технологии DDE (Dynamic Data Exchange — ди- намический обмен данными) — пользователь был вынужден запускать DDE-сер- вер вручную, что создавало массу неудобств — сервер запустить иногда забывали или достаточно долго искали к нему путь. Соответственно, при реализации СОМ возникла идея автоматизировать эту операцию — то есть если сервер не запущен, то его надо загрузить в память и запустить. Очевидно, что для этого необходимо где-то хранить полный путь к серверу. Подходящим для этого хранилищем оказался системный реестр. В нем можно указать имя и полный путь к СОМ-серверу. Таким образом, при обращении к серверу его местонахождение будет извлечено из системного реестра, и сервер будет запущен. Однако это еще не все. Клиент должен быть способен обратиться к СОМ-сер- веру для получения ссылки па интерфейсы. Как было сказано выше, для этого необходимо вернуть ссылку на какой-либо интерфейс, воспользовавшись мето- дом Queryinterface, у которого можно затребовать другие интерфейсы и начать вызывать их методы. Это означает, что экземпляр объекта, реализующего данный интерфейс, должен быть создан немедленно после старта COM-сервера. Соот- ветственно, экземпляр объекта должен удаляться только при окончании работы COM-сервера. Кроме того, интерфейс данного типа должен быть способен созда- вать экземпляры объектов, реализующих другие интерфейсы, в ответ на вызов метода Query Interface. Такой интерфейс имеется, и он называется фабрикой клас- сов (class factory) — ICIassFactory. Для того чтобы он был создан при старте COM-сервера, конструктор класса, реализующий фабрику классов, обычно по- мещается в секции initialization какого-нибудь модуля. Главный метод интерфейса ICIassFactory — метод Createlnstance. В качестве параметра он принимает IID интерфейса, ссылку па который необходимо вер- нуть клиенту, и переменную, куда помещается ссылка на требуемый интерфейс. При реализации метода Createlnstance необходимо проверить IID всех интерфей- сов, экспонируемых данной фабрикой классов. При совпадении с передаваемым в качестве параметра идентификатором возможно несколько вариантов реализации. Если ранее не был создан экземпляр класса, реализующий требуемый интер- фейс, то вызывается его конструктор. И Если уже имеется экземпляр класса, то возвращается ссылка на пего и счет- чик ссылок увеличивается па 1. В этом случае класс реализуется таким обра- зом, чтобы одновременно обслуживать нескольких клиентов.
Базовые понятия 45 Альтернативный вариант — при наличии уже работающей копии класса все равно создается новая копия. При таком способе работы число ссылок па каж- дую рабочую копию всегда равно единице (при равенстве пулю рабочая копия удаляется). Класс реализуется таким образом, чтобы обслуживать одного клиента. Типичный пример реализации метода Createlnstance приведен ниже: function TMyClassFactory.CreateInstance(UnkOuter; IUnknown; const lid: Tiid; var P): hResult; var hr HResult. MyObject:TMyObject; begin if UnkOuter <> nil then begin Result:=E_FAIL: Exit; end; if (not IsEqualIID(iid, IID_IUnknown)) and (not IsEqualIID(iid, CLSID_MyObject)) then begin Result ;= E_FAIL; Exit; end; MyObject ;=TMyObject.Create; if MyObject = nil then begin Pointer(Obj) := nil; Result := E_OUTOFMEMORY; Exit; end; hr := MyObject.Querylnterfacedid, P); if Failed(hr) then MyObject.Free else Inc(ObjCount); Result := hr; end; Первый параметр — UnkOuter — используется для создания агрегатов (их обсу- ждение выходит за рамки темы данной книги). В классе TMyObject реализован интерфейс IMyObject (как это сделано, будет рассказано ниже). Данный код создает новую рабочую копию класса TMyObject для каждого клиента. Обрати- те внимание па то, что для увеличения счетчика ссылок вызывается метод Query Interface интерфейса IMyObject. Для запуска COM-сервера могут использоваться два способа. Ж Вызов функции CoGetCl assObject, которой в качестве параметра передается IID фабрики классов. Эта функция находит в системном реестре имя и путь к серверу, который реализует данную фабрику классов, загружает и запускает его. Ссылка па фабрику классов передается клиенту. Клиент далее может
46 Глава 1. Основы технологии СОМ вызывать метод Createlnstance фабрики классов для получения экземпля- ров требуемых COM-серверов. Этот способ рекомендуется, если предполага- ется создать большое количество одинаковых объектов. н Вызов функции CoCreatelnstance, которая внутри себя вызывает метод CoGetClassObject, а затем автоматически вызывает метод Createlnstance фабрики классов для получения ссылки па требуемый интерфейс. Этот способ проще и его лучше использовать, если объект создается в одном экземпляре. Теперь следует рассмотреть данные, которые заносятся в системный реестр при регистрации СОМ-сервера. Данные о COM-сервере содержатся в секции HKEY_ CLASSES_ROOT. Первая секция сопоставляет CLSID объекта его строковому наиме- нованию (рис. 1.3). Рис. 1.3. Регистрация строкового имени СОМ-сервера в секции HKEY_CLASSES_ROOT Если СОМ-сервер имеет несколько фабрик классов, то для каждой из них создается такая секция. Функция ProgIDToCIassID использует эту информацию для нахождения IID требуемой фабрики классов. Для данного примера вызов функции ProgIDToCI assID('CheD.CheDApp') возвратит следующее значение: {A4C07FE2-47CA-11D1-8F71-D4AA05C10000} Следующая информация, которая помещается в системный реестр, — путь к COM-серверу и его имя (рис. 1.4). Рис. 1.4. Регистрация имени и пути к COM-серверу в секции HKEY_CLASSES_ROOT Эта информация помещается в секцию, заголовок которой совпадает с иден- тификатором CLSID объекта.
Базовые понятия 47 Если такая секция находится и фабрика классов поддерживает режим от- дельного экземпляра (single instance), то приложение загружается и запускается. Режим отдельного экземпляра означает, что в ответ па требование клиентов о пе- редаче ссылки на фабрику классов запускается отдельная копия сервера. В этом случае при работе с многочисленными клиентами на компьютере выполняется несколько копий сервера одновременно. Если же фабрика классов поддерживает режим множественных экземпляров (multiple instances), то первоначально прове- ряется, выполняется ли уже COM-сервер на данном компьютере. Если сервер уже запущен, то клиенту передается ссылка на требуемую фабрику классов, а если пет — то происходит загрузка, запуск и передача ссылки. В таком режиме па компьютере выполняется всегда одна копия сервера, независимо от того, сколько клиентов к нему обратилось. Серверы в виде исполняемых файлов, написанные на Delphi, автоматически регистрируются при первом запуске программы на компьютере. Для регистра- ции серверов DLL служит утилита Regsvr32, поставляемая в составе Windows, либо TRegSvr из поставки Delphi. СОМ и потоки выполнения Поскольку Windows — многопоточная и многозадачная операционная система, СОМ-клиепт и COM-сервер могут оказаться в различных процессах (processes) или потоках выполнения (threads) приложения, и к серверу может обращаться не- сколько клиентов. Технология СОМ решает эту проблему при помощи концепции апартаментов (apartments), в которых выполняются COM-клиенты и СОМ-сер- веры. Апартаменты бывают однопоточпые (Single Threaded Apartment, STA), многопоточные (Multiple Threaded Apartment, MTA) и нейтральные (Neutral apartment). При создании однопоточного апартамента организуется очередь вызовов ме- тодов, и каждый из них обрабатывается только после того, как будут обработаны все предшествующие вызовы. STА, за редким исключением, является наиболее подходящим выбором для реализации COM-серверов. Использовать МТА есть смысл только в том случае, если STA не подходит для конкретного сервера. Внутри многопоточного апартамента может быть создано сколько угодно по- токов и объектов, причем каждый объект не привязывается к какому-то конкрет- ному потоку, и любой метод объекта может быть вызван в любом из потоков. СОМ автоматически ведет пул потоков внутри МТА и при вызове со стороны клиента находит свободный поток, в котором вызывает метод требуемого объекта. COM-сервер, работающий в МТА, обладает потенциально более высоким бы- стродействием и доступностью для клиентов, однако он значительно сложнее в разработке. Нейтральные апартаменты используются при создании серверов СОМ+. Ме- тоды объектов в таких апартаментах вызываются в потоке, из которого к ним обратился клиент. Это позволяет уменьшить количество переключений потоков. Более подробно о работе с потоками выполнения будет рассказано в главе 6.
48 Глава 1. Основы технологии СОМ Активация сервера Для активации COM-сервера клиент должен вызвать функцию CreateComObject, описанную в модуле ComObj.pas: function CreateComObject(const ClassID: TGUID): IUnknown: Функция получает в качестве параметра CLSID требуемого объекта и возвра- щает ссылку па его интерфейс IUnknown. Далее клиент может запросить требуе- мый интерфейс и работать с ним. var COMServer: IComServer: // Создаем COM-объект и запрашиваем у него интерфейс ComServer := CreateComObject!IComServer) as IComServer: // Работаем с интерфейсом ComServer.DoSomethi ng: // Освобождаем интерфейс ComServer := nil; Что же делает СОМ при запросе па создание сервера? 1. В реестре по запрошенному идентификатору CLSID ищется запись регистра- ции сервера. 2. В этой записи находится имя файла модуля сервера. □ Если это исполняемый файл — он запускается на выполнение. Любое при- ложение, реализующее COM-сервер при старте, регистрирует в системе интерфейс фабрики классов. После запуска и регистрации СОМ получает ссылку на фабрику классов. Фабрика классов — это COM-сервер, реа- лизующий интерфейс IClassFactory. Ключевым методом этого интерфейса является метод Create Instance, который и создает экземпляр требуемого объекта. □ Если это библиотека, опа загружается в адресное пространство вызвав- шего процесса и вызывается ее функция 01 IGetCl assObject, возвращающая ссылку па реализованную в DLL фабрику классов. 3. СОМ вызывает метод Createlnstance и передает полученный интерфейс кли- енту. По завершении работы с COM-объектом клиент освобождает ссылку на него (что приводит к вызову метода Rel ease). В этот момент COM-сервер проверяет, есть ли еще ссылки на созданные им объекты. Если все объекты освобождены, то COM-сервер завершает свою работу. В случае если сервер реализован в виде DLL, он должен экспортировать функцию DllCanUnloadNow, которая вызывается СОМ по таймеру или при вызове функции CoFreeUnusedLibraries. Если все объекты из этой библиотеки освобождены, то опа выгружается из памяти.
Базовые понятия 49 Вся работа по созданию и регистрации фабрики объектов и экспорту соответ- ствующих функций из DLL в Delphi уже реализована в составе стандартных библиотек, поэтому создание COM-сервера в действительности является очень простой задачей. Поддержка Delphi стандартных интерфейсов СОМ В Delphi уже имеется ряд классов, реализующих интерфейсы, необходимые для создания стандартных СОМ-объектов — серверов автоматизации и элементов управления ActiveX: TComObject -> TTypedComObject -> TAutoObject -> TActiveXControl Поскольку интерфейсы, реализуемые в этих объектах, могут быть затребованы другими модулями, каждому из них необходима своя фабрика классов. Эти фаб- рики классов также определены и реализованы в классах: TComObjectFactory -> TTypedComObjectFactory -> TAutoObjectFactory -> TActiveXControl Factory Класс TComObject реализует два интерфейса — IUnknown и ISupportErrorlnfo. По- следний используется для передачи информации об ошибках СОМ-клиенту. Этот класс требуется мастеру Delphi, когда разработчик на странице ActiveX репозитария объектов выбирает значок COM Object, предварительно выбрав в меню команду File ► New ► Other. В классе TTypedComObject добавляется поддержка интерфейса IProvideCl assinfo, который имеет единственный (по сравнению с IUnknown) допол- нительный метод — GetClassInfo. Этот метод возвращает указатель на интерфейс ITypelnfo, который, как уже говорилось, используется для получения информации о библиотеке типов. Начиная с этого класса, уже можно описывать СОМ-объекты с библиотеками типов. В следующем классе — TAutoObject — добавляется реализа- ция интерфейса IDispatch. Именно этот класс требуется мастеру Delphi при выборе па странице ActiveX окна репозитария объектов (открывается командой File ► New ► Other) значка Automation Object. И наконец, в классе TActiveXControl добавля- ется реализация интерфейсов IConnectionPointContainer, IDataObject, WbjectSafety, lOleControl, IDlelnPlaceActiveObject, lOlelnPlaceObject, lOleObject, IPerPropertyBrowsing, IPersistPropertyBag, IPersistStorage, IPersistStreamlnit, IQuickActivate, ISimpl eFrameSite, ISpeci fyPropertyPages, IViewObject, IView0bject2, необходимых для поддержки элемен- тов управления ActiveX. Использование некоторых из этих интерфейсов будет рассмотрено в следующих главах. Библиотека типов и информация о методах сервера В данном разделе рассказано о том, как COM-сервер может информировать среду разработки об интерфейсах, их идентификаторах GUID, списке поддерживаемых методов и параметрах этих методов. В самом деле, COM-клиент создается после создания COM-сервера. Было бы разумно получить от COM-сервера список его
50 Глава 1. Основы технологии СОМ методов и список формальных параметров этих методов. Это позволило бы осуществить синтаксический контроль на этапе разработки клиента. Кроме того, названия методов и формальных параметров несут в себе смысловую нагрузку: разработчик легче их воспринимает, поскольку часто назначение методов и па- раметров очевидно. Также список методов и параметров определенных пользо- вателем интерфейсов необходим для доступа к виртуальной таблице методов интерфейса — реализации так называемого раннего связывания (early binding). Данная проблема решается при помощи библиотеки типов. Реализация библио- теки типов не обязательна — при ее отсутствии доступ к методам СОМ-сервера осуществляется на основании поставляемой с сервером документации. Библио- тека типов представляет собой список фабрик классов, поддерживаемых данным COM-сервером. Для каждой фабрики классов можно получить список интерфей- сов. Для каждого интерфейса приводится список поддерживаемых методов, для каждого из которых можно затребовать список формальных параметров. Поря- док следования методов в списке соответствует их порядку следования в вирту- альной таблице методов — поэтому на этапе компиляции клиента можно связать вызов метода сервера с соответствующим столбцом виртуальной таблицы. Все эти списки сохраняются либо непосредственно в COM-сервере (в фай- лах с расширениями *.ехе, * .dll, *.осх), либо отдельно (в файлах с расширениями *.tlb, *.olb). Для их хранения предусмотрены специальные двоичные форматы. Соответственно, инструменты программирования, поддерживающие импорт биб- лиотек типов, должны знать форматы этих файлов, уметь читать данные из них и представлять эти данные разработчику в естественном, специфическом для дан- ного языка программирования виде. Например, если библиотека типов импорти- рована в средство разработки, использующее компилятор C++, то ее реализа- ция должна быть представлена с помощью синтаксиса C++; если импортирована в Delphi — с помощью синтаксиса Delphi. При этом разработчик должен отдавать себе отчет, что реально библиотека типов хранится совсем в других форматах. Если же описание библиотеки типов хранится непосредственно в СОМ-сер- вере, то на сервере можно реализовать интерфейс ITypeInfo. Пользуясь методами этого интерфейса, среда разработки может опросить сервер о тех же самых пара- метрах, что считываются из вышеперечисленных файлов. Пользоваться интер- фейсом ITypelnfo гораздо проще, чем писать конверторы форматов, и опытный разработчик может легко узнать о содержимом библиотеки типов сервера в кли- ентском приложении. Соответственно, сама библиотека типов в этом случае мо- жет храниться в сервере в произвольном формате. Для создания и редактирования библиотек типов в Delphi имеется редактор, совмещенный с редактором интерфейсов (определение фабрик классов, интер- фейсов и их методов). Он будет достаточно детально описан при рассмотрении примеров в этой книге. Язык IDL Многие средства разработки, используемые для создания COM-серверов, содержат в комплекте поставки утилиты для автоматической генерации библиотек типов или клиентского и серверного кода на основании описаний интерфейсов сервера.
Базовые понятия 51 Во многих случаях эти описания создаются па языке IDL (Interface Definition Language — язык определения интерфейсов). Зачем нужен язык IDL? Для спецификации интерфейсов. Фактически это стандарт, позволяющий описывать вызываемые методы сервера и их параметры, не вдаваясь в детали и правила реализации серверов и клиентов на том или ином языке программирования. Используя IDL, можно описать интерфейсы сервера, а затем создать его реализацию, равно как и реализацию клиента, на любом языке программирования с помощью широкого спектра средств разработки. В опреде- ленном смысле IDL — это стандарт для описания взаимодействия между компо- нентами распределенной системы, не зависящий от деталей реализации и языков программирования (а в общем случае не зависящий также от платформ, поскольку IDL используется не только в СОМ). Язык IDL в технологии СОМ является преемником этого языка в технологии DCE (Distributed Computing Environment — среда распределенных вычислений) — спецификации межплатформеппого взаимодействия служб, разработанной кон- сорциумом Open Systems Foundation. Отметим, что в настоящее время сущест- вует несколько диалектов IDL (для СОМ, для CORBA, для DCE и др.). Тем не менее различия между ними невелики. Язык IDL немного похож па язык C++ в той его части, которая относится к опи- санию классов (то, что обычно помещается в h-файлы). В качестве примера приведем описание на IDL интерфейсов гипотетического COM-сервера, содержа- щего объект MyComObj, интерфейс которого IMyComObj (наследник IUnknown) экспо- нирует два метода: метод MyMethodl, получающий в качестве входных параметров два целых числа и возвращающий действительное число, и метод MyMethod2, не возвращающий данных и получающий в качестве входного параметра перемен- ную типа Variant: [ uuid(845256DO-8E96-HD2-B126-OOOOOOOODOOO). version(l.O). helpstringCProjectl Library”) ] library Projectl { importlib(”STDOLE2.TLB"); importlib("STDVCL40.DLL"); [ I uuid(845256Dl-8E96-1102-B126-000000000000). version(l.O). helpstringC’Interface for myComObj Object”) ] interface ImyComObj: IUnknown { [id(OxOOOOOOOl)] double _stdcall MyMethodl([in] long Paraml, [in] long Param2 ):
52 Глава 1. Основы технологии СОМ [id(0x00000002)] void _stdcall MyMehod2([in] VARIANT РагатЗ ): }: [ uuid(845256D3-8E96-HD2-B126-000000000000). version(l.O) helpstringC’myComObj Object") ] coclass myComObj { [default] interface ImyComObj; }; }: Многие средства разработки, поддерживающие создание COM-серверов, имеют в своем составе утилиты для генерации серверного и клиентского кода на осно- вании описаний на IDL. В частности, в состав Microsoft Visual С ++ включен компилятор MIDL, генерирующий код для клиентских и серверных библиотек, ответственных за взаимодействие клиента и сервера, на основании созданных разработчиками описаний на IDL. Отметим, однако, что при создании COM-серверов с помощью мастеров Delphi библиотеки типов и соответствующий код (или его «заготовки») генерируются автоматически и нет необходимости вручную создавать описания па IDL. При этом всегда можно сгенерировать описание на IDL па основании библиотеки типов сервера, созданной с помощью соответствующего редактора. Создание СОМ-сервера Для создания СОМ-сервера Delphi предоставляет широкий набор мастеров, ав- томатизирующих выполнение рутинных задач и позволяющих программисту сконцентрироваться на реализации требуемой функциональности. Мастера до- ступны на странице ActiveX окна репозитария объектов, доступ к которому от- крывает команда File ► New ► Other (рис. 1.5). Чтобы сделать COM-сервером ЕХЕ-файл, следует просто добавить в сервер модуль с COM-объектом. Для создания СОМ-сервера в виде DLL нужно сначала создать библиотеку, оформленную с учетом требований СОМ. Это делается при помощи значка ActiveX Library. При его выборе будет создан новый проект, реали- зующий DLL, и сгенерирован следующий код: library Projectl; uses ComServ: exports DIIGetClassObject,
Создание COM-сервера 53 DllCanUnloadNow, Dll Registerserver, DI 1 UnregisterServer: {$R *.RES} begin end. Рис. 1.5. Набор мастеров для создания СОМ-серверов Созданная библиотека экспортирует функции, необходимые для работы СОМ, поэтому можно не заботиться о рутинной работе и сразу приступить к реализа- ции COM-сервера, выбрав значок COM Object Wizard (рис. 1.6). От заполнения окна мастера создания COM-объекта зависит реализация соз- даваемого СОМ-объекта. В поле Class Name вводится имя класса Delphi, реализующего СОМ-сервер. Мастер создаст заготовку класса с этим именем. Под этим же именем СОМ- сервер будет зарегистрирован в реестре. Раскрывающийся список Instancing позволяет выбрать режим создания СОМ- объектов. Доступны следующие значения (выбранное значение имеет смысл только для ЕХЕ-серверов, для DLL оно игнорируется): □ Internal — объект может использоваться только внутри этого приложения; □ Single Instance — создание каждого экземпляра объекта приводит к запуску нового экземпляра приложения-сервера; после создания объекта фабрика классов приложения удаляет информацию о себе из системного списка за- регистрированных фабрик, что заставляет СОМ при создании нового объ- екта запустить приложение сервер в новом процессе;
54 Глава 1. Основы технологии СОМ □ Multiple Instance — после создания экземпляра объекта фабрика классов не удаляет себя из списка зарегистрированных; при построении запроса на создание нового объекта СОМ обнаружит ее в этом списке и запросит у той же фабрики новый экземпляр объекта, который будет создан в том же приложении (другими словами, для создания всех объектов данного типа будет запущено не более одного экземпляра сервера). Рис. 1.6. Окно мастера создания СОМ-объекта Раскрывающийся список Threading Model позволяет выбрать модель потоков сервера. Подробно модели потоков будут обсуждаться в главе 6. Поле Implemented Interfaces доступно только в том случае, если объект не ис- пользует библиотеку типов. В этом случае вы должны сами описать интер- фейсы в коде своей программы и через запятую перечислить их в этом поле, например: ITest, lAnotherTest Кнопка List позволяет выбрать интерфейс из числа зарегистрированных па данном компьютере. В поле Description при желании вводят описание объекта. Ж Установка флажка Include Type Library приводит к включению в сервер библио- теки типов — специального двоичного ресурса, описывающего реализуемые сервером интерфейсы, их методы и параметры вызова. СОМ предоставляет стандартные средства работы с библиотеками типов. В частности, Delphi может импортировать имеющуюся в сервере библиотеку типов и автсьматически по- строить по ней интерфейсный модуль для работы с ним. При использовании библиотеки типов интерфейсы описываются при помощи редактора библиотек типов. Если флажок установлен, объект наследуется от класса TTypedComObject, если флажок спят — от класса TComObject (это облегченная реализация сервера).
Создание СОМ-сервера 55 Установка флажка Mark interface Oleautomation делает СОМ-сервер совмес- тимым с технологией OLE Automation. В этом случае следует использовать в методах интерфейса только совместимые с технологией OLE Automation типы данных. Это необходимо, если нужно передавать параметры вызова интерфейса между разными апартаментами. Такая операция, называемая маршалингом (marshalling), в общем случае требует написания специальных библиотек, реализующих функциональность прокси (proxy) и стаба (stub) — объектов, ответственных за обмен данными между клиентом и сервером. Одна- ко если установить флажок Mark interface Oleautomation, эту задачу решает маршалер OLE, избавляя разработчика от лишней работы. ВНИМАНИЕ ------------------------------------------------------------ Для поддержки маршалинга, совместимого с OLE Automation, необходимо, во-первых, чтобы сервер был унаследован от класса TTypedComObject (реализация интерфейса IDispatch не обязательна), во-вторых, чтобы все методы интерфейса были объявлены как процедуры, удовлетворяющие соглашению о вызове safecall, либо как функции, удовлетворяющие соглашению о вызове safecall и возвращающие значение типа HRESULT. Если вы создаете интерфейс, унаследованный от интерфейса IUnknown, то по умолча- нию все его методы объявляются как удовлетворяющие соглашению о вызове stdcall. Чтобы все-таки создавать методы, удовлетворяющие соглашению о вызове safecall, необходимо па странице Type Library диалогового окна Environment Options (открывает- ся командой Tools ► Environment Options) установить переключатель All v-table interfaces в группе Safecall function mapping. Сервер без библиотеки типов Сервер без библиотеки типов, если он не реализует интерфейс IMarshal, может ра- ботать только в одном апартаменте с клиентом, поэтому его следует использовать только для внутрипроцессных серверов и с моделью потоков, идентичной клиенту. При создании сервера, не включающего библиотеку типов, следует указать мастеру реализуемые сервером интерфейсы. Укажем имя интерфейса ITest. По завершении работы мастера будет создан следующий модуль: unit Unitl: interface uses Windows. ActiveX, Classes, ComObj: type TTest = c1ass(TCom0bject. ITest) protected end: const Class_Test: TGUID = ’{1302FB00-703F-11D4-84DD-825B45DBA617}':
56 Глава 1. Основы технологии СОМ Implementation uses ComServ: initialization TComObjectFactory.Create(ComServer, TTest. ClassJTest. 'Test', '', ciMultiInstance. tmApartment): end. ВНИМАНИЕ ----------------------------------------------------------------------- Если вы создаете СОМ-сервер, ориентированный на использование различными кли- ентами (а пе только в рамках конкретного проекта, в котором спецификации клиентов жестко заданы), пе рекомендуется делать серверы без поддержки маршалинга данных, поскольку невозможно будет гарантированно обеспечить его нахождение в одном апартаменте с клиентом. Если вы все же создаете такой сервер, отразите требуемые для клиента спецификации в документации па сервер. Посмотрим па сгенерированный код подробнее. Особый интерес вызывает сек- ция initialization. В пей создается экземпляр фабрики классов — СОМ-сервера, реализующего интерфейс IClassFactory2. К нему СОМ будет обращаться для создания экземпляра объекта Test. Всю рутинную работу по взаимодействию с СОМ автоматически реализует библиотека VCL. Для создания сервера требуется написать интерфейсный модуль с описани- ем реализуемого интерфейса. Кроме этого, вынесем в пего описание константы Class_Test и добавим его в секцию uses модуля Unitl: unit Testinterface; interface const Class_Test: TGUID = ‘{1302FB00-703F-11D4-84DD-825B45DBA617}’; type ITest = interface [' {1C986802-6D6D-11D4-84DD-996A491CE716}'] procedure ShowItCS: String); end; implementation end. Этот модуль содержит всю необходимую информацию для использования сервера и должен применяться при компиляции клиента. Дополним код COM-объекта реализацией методов требуемого интерфейса: unit Unitl; interface
Создание COM-сервера 57 uses Windows, ActiveX, Classes, ComObj, Testinterface: type TTest = class(TComObject. ITest) protected procedure ShowItCS: String); end; implementation uses ComServ: { TTest } procedure TTest.ShowIt(S: String); begi n MessageBox(0. PChar(S), nil. 0): end; initialization TComObjectFactory.Create(ComServer, TTest. Classjest, 'Test', ". ciMultiInstance. tmApartment): end. Откомпилировав проект, мы получим файл Projectl .dll. Последним шагом является регистрация COM-сервера. Для регистрации вве- дем в командной строке следующее: regsvr32 projectl.dll Если все было проделано правильно, па экране должно появиться сообщение об успешной регистрации: Dll Registerserver in Projectl.dll succeeded Можно приступать к написанию клиента. Для этого создадим новый проект, добавим в модуль с его главной формой строку uses Test Interface и напишем сле- дующий код: uses TestInterface, ComObj; procedure TForml.ButtonlClickCSender: TObject): var Test: ITest: begin Test := CreateComObject(Class_Test) as ITest: Test.ShowItC'Hi'); end:
58 Глава 1. Основы технологии СОМ Как следует из этого примера, создание и использование COM-сервера пе сложнее, чем работа с обычными классами Delphi. СОМ-сервер без библиотеки типов является хорошим выбором для реализации внутри проекта, поскольку для применения такого сервера нужен интерфейсный модуль. При передаче сер- вера другим разработчикам вам придется передать им этот модуль и при необхо- димости перевести его на другой язык (например, С). Сервер с библиотекой типов Библиотека типов — это специальный двоичный ресурс, описывающий интер- фейсы и методы, реализуемые COM-сервером. Кроме наличия библиотеки ти- пов, сервер должен поддерживать интерфейс IProvideCl assinfo. В Delphi такой сервер реализуется путем наследования его от класса TTypedComObject. Для этого следует оставить установленным флажок Include Type Library в окне мастера со- здания СОМ-объекта. Создадим СОМ-сервер в виде исполняемого файла (разумеется, он может быть также создан и в виде DLL). Сначала создадим новый проект (команда File ► New Application), а затем доба- вим в него СОМ-объект. Если не снимать флажок Include Type Library, то мастер создаст уже не один, а два модуля. Первый из них напоминает созданный ранее: unit Unitl; interface uses Windows. ActiveX, Classes. ComObj. Projectl_TLB, StdVcl; type TTestl = class(TTypedComObject. ITestl) protected {Declare ITestl methods here} end; implementation uses ComServ; initialization TTypedComObjectFactory.Create(ComServer. TTestl. Class_Testl. ciMultiInstance, tmApartment): end. Наиболее интересна строка: uses...Projectl_TLB. Это автоматически сгенери- рованный интерфейсный модуль к нашему COM-объекту (аналогично файлу Testinterface.pas в предыдущем примере). Он содержит описание всех необходи- мых для работы с сервером интерфейсов. В отличие от предыдущего примера,
Создание СОМ-сервера 59 его не требуется редактировать вручную. Для этого Delphi откроет редактор биб- лиотеки типов (рис. 1.7). Рис. 1.7. Редактор библиотеки типов Это специализированный редактор для описания интерфейсов СОМ-объек- тов. Мы должны описать все требуемые интерфейсы, их свойства и методы в этом редакторе, после чего можно щелкнуть на кнопке Refresh панели инстру- ментов редактора, и изменения будут автоматически внесены во все требуемые модули. Затем останется лишь дописать реализацию методов. Добавим описание нового метода. Для этого щелкнем правой кнопкой мыши па интерфейсе ITest и выберем в контекстном меню команду New ► Method. На странице Attributes редактора библиотеки типов введем имя метода Showlt. Перейдем па страницу Parameters и добавим к методу Showlt параметр S типа BSTR, как показано на рис. 1.8. После этого щелкнем на кнопке Refresh и посмотрим на исходные тексты па- шей программы. В модуле Projectl_TLB в описании интерфейса ITest 1 появился метод Showlt: 1 Testi = interface!IUnknown) [{1302FB06-703F-11D4-84DD-825B45DBA617}'] function Showlt(const S: WideString): HResult: stdcall: В модуле Unitl также появились новые строки: type TTestl = cl ass(TTypedComObject, ITestl) protected function Showlt(const S: WideString): HResult; stdcall, end:
60 Глава 1. Основы технологии СОМ implementation uses ComServ: function TTestl.ShowIt(const S: WideString): HResult: begin end: Рис. 1.8. Добавление параметров метода в интерфейсе Нам остается лишь написать реализацию метода: function TTestl.ShowIt(const S: WideString): HResult: begin MessageBoxWCO. PWideChar(S). nil, 0) Result := S_OK: // Стандартный код успешного завершения end; Для регистрации сервера достаточно один раз запустить его на компьютере клиента. Создание СОМ-клиента Перейдем к написанию приложения-клиента. Если у пас есть модуль Project_TLB, то оно ничем не будет отличаться от предыдущего примера. Более интересен слу- чай, когда мы имеем только исполняемый файл с сервером. Зарегистрируем этот сервер и выберем в меню Delphi IDE команду Project ► Import Type Library.
Создание COM-клиента 61 В открывшемся окне найдем строку с описанием библиотеки типов требуемого сервера (рис. 1.9). Рис. 1.9. Импорт библиотеки типов Если установлен флажок Generate Component Wrapper, то в импортированный модуль будет добавлен код для создания компонента Delphi, который можно по- местить на форму, и он автоматически создаст требуемый СОМ-сервер и позво- лит обращаться к его методам. В противном случае будет сгенерирован модуль, содержащий описание всех имеющихся в библиотеке типов интерфейсов. Далее следует определить, что нужно сделать с выбранной библиотекой. М Щелчок па кнопке Install создает модуль с описанием интерфейсов и автома- тически регистрирует требуемые компоненты в IDE. После этого остается лишь поместить их на форму. Щелчок на кнопке Create Unit создает интерфейсный модуль, но пе устанав- ливает его в IDE. Это удобно, если вам нужны только описания интерфейсов или если вы хотите вручную установить его в пакет (package) компонентов, отличающийся от используемого по умолчанию. Таким образом, для распространения и использования сервера пе нужно ни- чего, кроме его исполняемого модуля. Но главное даже не это. Гораздо важнее,
62 Глава 1. Основы технологии СОМ что можно импортировать и использовать в своей программе любой из имею- щихся па компьютере COM-серверов. Естественно, что при передаче своей про- граммы клиенту следует установить на компьютере клиента соответствующий СОМ-сервер. Для примера используем в своем приложении процессор регулярных выра- жений VBScript. Импортируем библиотеку типов Microsoft VBScript Regular Expressions, вы- брав команду Project ► Import Type Library в главном меню среды разработки и сняв флажок Generate Component Wrapper в открывшемся диалоговом окне. При этом будет создан файл VBScript_RegExp_TLB.pas. Создадим форму и добавим следую- щий код для проверки, входит ли в поле Edit2 текст, введенный в поле Editl: uses VBScri pt_RegExp_TLB; procedure TForml.ButtonlClick(Sender: TObject): var RE: IRegExp; begin RE := CoRegExp.Create; RE.Pattern := Editl.Text: if RE.Test(Edit2.Text) then Caption := 'TRUE' el se Caption := 'FALSE': end: Это все! Мы обеспечили в своем приложении поддержку регулярных выра- жений, причем такую же, как в языках сценариев Microsoft (VBScript и JScript). Создание модуля расширения в виде СОМ-сервера Попробуем теперь реализовать модуль расширения (plug-in) к своей программе в виде СОМ-сервера и сравним код, полученный в этом случае, с кодом, написан- ным при «ручном» программировании. Вначале создадим модуль с описанием интерфейсов: unit Plugininterface: interface const Class_TAPI: TGUID - '{A132D1A1-721C-11D4-84DD-E2DEF6359A17}’; type IAPI = interface
Создание модуля расширения в виде СОМ-сервера 63 [1{64CFF1E0-61АЗ-11D4-84DD-B18D6F94141F}'] procedure ShowMessage(const S: String); end; ILoadFilter = interface [’{64CFF1E1-61A3-11D4-840D-B1806F94141F} ’ ] procedure Init(const FileName; String): function GetNextLine(var S: String): Boolean; end; implementation Л end. Обратите внимание, что метод ILoadFilter.Init больше не получает ссылки па внутренний API программы — он будет реализован в виде СОМ-объекта. Создадим DLL с COM-сервером, реализующим интерфейс ILoadFilter. Для этого создадим новую библиотеку ActiveX и добавим в нее COM-объект TLoadFilter. В раскрывающемся списке Threading Model выберем пункт Single, означающий выбор модели одного потока (single-threaded model), поскольку использование сервера в потоках выполнения не предполагается. После этого реализуем методы интерфейса ILoadFilter. unit Unit3; interface uses Windows, ActiveX. Classes. ComObj. Plugininterface; type TLoadFilter = class(TComObject. ILoadFilter) private FAPI; IAPI; F; TextFile; Lines: Integer; InitSuccess: Boolean; protected procedure Init(const FileName: String); function GetNextLine!var S: String): Boolean; public destructor Destroy: override; end; const Class_LoadFilter: TGUID = ’{A132D1A2-721C-11D4-84DD-E2DEF6359A17}’;
64 Глава 1. Основы технологии СОМ implementation uses ComServ. Syslltils: Деструктор и метод GetNextLi пе те же, что и в предыдущем примере: destructor TLoadFiIter.Destroy: begin if InitSuccess then CloseFile(F): inherited; end: function TLoadFi1 ter.GetNextLine(var S: String): Boolean: begin if InitSuccess then begin Inc(Lines); Result := not Eof(F): if Result then begin Readln(F, S): FAPI.ShowMessage('Загружено ' + IntToStr(Lines) + ' строк.'): end: end else Result := False: end; В методе Init имеется существенное отличие — теперь ссылку на внутренний API программы мы получаем при помощи СОМ. Это освобождает пас от необхо- димости передавать ссылку в модуль расширения: procedure TLoadFilter.InitCconst FileName: String): begin FAPI := CreateComObject(Class_TAPI) as IAPI; AssignFileCF, FileName): Reset(F): {*!+} InitSuccess := IGResult = 0; if not InitSuccess then FAPI.ShowMessage('Ошибка инициализации загрузки'); end: В конце модуля содержится код, автоматически сгенерированный Delphi для создания фабрики объектов: initialization TComObjectFactory.Create(ComServer. TLoadFi1 ter. Class_LoadFilter, 'LoadFilter'. ". ciMultiInstance, tmSingle): end.
Создание модуля расширения в виде СОМ-сервера 65 Скомпилируем библиотеку и зарегистрируем ее при помощи утилиты regsvr32. Поскольку программа может поддерживать множество различных фильтров, организуем их подключение через файл инициализации следующего вида: [Filters] TXT={A132D1A2-721C-11D4-84DD-E2DEF6359A17} Параметром строки служит CLSID сервера, реализующего фильтр. В пашем случае это будет константа Class_LoadFilter. Для подключения дополнитель- ных фильтров необходимо создать DLL с сервером, реализующим интерфейс ILoadFilter, зарегистрировать созданную библиотеку в реестре и добавить CLSID сервера в файл инициализации. Теперь можно приступать к написанию программы-клиента. Опа аналогична программе из предыдущего примера. Добавим в нее СОМ-сервер, реализующий внутренний API. За исключением кода, сгенерированного СОМ, объект полностью аналогичен объекту, приведенному ранее. Константу Class_TAPI вынесем в модуль Plugininterface, чтобы сделать ее доступной для модулей расширения. unit Unit2: interface uses Windows. ActiveX, Classes. ComObj. Plugininterface: type TTAPI = classCTComObject. IAPI) protected procedure ShowMessagelconst S: String): end; implementation uses Forms. ComServ. Unitl; { TTAPI } procedure TTAPI.ShowMessage(const S: String); begin (Application.MainForm as TForml).StatusBarl.SimpleText := S: end: initialization TComObjectFactory.Create(ComServer. TTAPI. Class_TAPI. 'TAPI', ". ciMultiInstance. tmSingle): end.
66 Глава 1. Основы технологии СОМ Теперь все готово к реализации функциональности клиента. Для экономии места приведем лишь метод LoadData: var PluglnName: String; Filter: ILoadFilter; S. Ext: String: begi n Memol.Lines.Cl ear; Memol.Li nes.Begi nUpdate; try Ext := ExtractFileExt(FHeName): DeleteCExt. 1. 1); with TIniFile.Create(ExtractF11ePath(ParamStr(0)) + 'plugins.ini’) do try PluglnName := Readstring ('Filters', Ext. "): finally Free; end; Filter ;= CreateComObject(StnngToGUID(PlugInName)) as ILoadFilter: Filter.Init(FileName): while Filter.GetNextLine(S) do Memol.Lines.Add(S): finally Memol.Li nes.EndUpdate; end; end: Очевидно, что код метода стал гораздо короче и проще. Всю черновую работу по поиску, загрузке и выгрузке DLL, поиску и созданию объектов берет па себя СОМ. ВНИМАНИЕ ------------------------------------------------------------------------- Поскольку в EXE и DLL используются длинные строки, не забудьте включить в сек- цию uses обоих проектов модуль Sha reMem. Автоматическая регистрация серверов из приложения Удобно в своей программе автоматически регистрировать все необходимые сер- веры. Это можно сделать при помощи следующей процедуры: procedure CheckComServerInstalled(const CLSID: TGUID; const Dll Name: String); var Size: Integer;
Технология OLE Automation 67 Dll Handle: THandle: FileName: String: begin Size := MAX_PATH: SetLength(FileName, Size); try if RegQueryValue(HKEY_CLASSES_ROOT. PChar(Format( 'CLSIDUs\InProcServer32'. [GUIDToString(CLSID)])). PChar(FileName), Size) = ERROR_SUCCESS then begin SetLengthCFileName. Size): DllHandle := LoadL1brary(PChar(F11eName)): FreeLibrary(Dl 1 Handle): if DllHandle = 0 then begin RegDel eteKey(HKEY_CLASSES_ROOT. PCha r(Format(' CLSIDUs ’, [GUIDToStri ng(CLSID)]))): RegisterComServer(Dll Name): end: end else begin RegisterComServer(DllName): end: except raise Exception.CreateFmt('He могу зарегистрировать' + '%s.'. [DllName]); end; end: В процедуре осуществляется дополнительная проверка наличия па диске файла с зарегистрированным сервером. Если файл пе найден по указанному в реестре месту, данные о регистрации удаляются и предпринимается попытка зарегистри- ровать сервер заново. Такая проверка очень полезна при переносе DLL с серве- ром в другую пайку на диске. Технология OLE Automation Стандарт СОМ основан па едином для всех поддерживающих его языков формате таблицы, описывающей ссылки па методы объекта, реализующего интерфейс. Однако вызов методов при помощи этой таблицы доступен только для компи- лирующих языков программирования. В то же время очень удобно было бы иметь доступ ко всем предоставляемым СОМ возможностям из интерпретирую- щих языков, таких как VBScript. Для поддержки этих языков была разработана технология под названием OLE Automation (автоматизация OLE, или просто автоматизация), позволяющая приложениям делать свою функциональность доступной для гораздо большего числа клиентов. Технология OLE Automation
68 Глава 1. Основы технологии СОМ базируется па технологии СОМ и является ее подмножеством, сднако наклады- вает на COM-серверы ряд дополнительных требований: Ж интерфейс, реализуемый COM-сервером, должен наследоваться от интерфейса IDispatch; » должны использоваться типы данных из числа поддерживаемых технологией OLE Automation (табл. 1.1); К все методы интерфейса должны быть либо процедурами, поддерживающими соглашение о вызове safecall, либо функциями, поддерживающими соглаше- ние о вызове safecall и возвращающими значение типа HRESULT; Я для поддержки пользовательских типов данных должен быть реализован ин- терфейс IRecordlnfo. ПРИМЕЧАНИЕ ---------------------------------------------------------- Помимо интерфейса I Record Info серверы автоматизации могут поддерживать еще ряд интерфейсов, позволяющих получать информацию о методах, обрабатывать ошибки и т. и. Все необходимые интерфейсы реализуются VCL Delphi автоматически. Таблица 1.1. Типы данных OLE Automation Тип (Pascal) Тип (IDL) Описание Byte Byte 1 байт, целое без знака, диапазон от 0 до 255 Currency CURRENCY 8 байт, с плавающей запятой и 4 знаками после запятой, диапазон от -922 337 203 685 477,5808 до 922 337 203 685 477,5807, сопроцессорный тип DISPPARAMS DISPPARAMS Структура, содержащая параметры вызова методов через метод Invoke интерфейса IDispatch. Расшифровка структуры приведена в модуле ActiveX, pas Double Double 8 байт, с плавающей запятой, диапазон от 5,0х10 324 до 1,7х1О308, 15-16 знаков EXCEPINFO EXEPINFO Структура, содержащая информацию об исключении. Расшифровка приведена в модуле ActiveX.pas GUID GUID Глобально уникальный идентификатор (класса, интерфейса). Структура размером 16 байт Hresult HRESULT 4 байта, целое число без знака, диапазон от 0 до 4 294 967 295 Integer Long 4 байта, целое число со знаком, диапазон от 2 147 483 648 до 2 147 483 647 Largeuint unsigned int64 8 байт, целое число со знаком, диапазон от 0 до 264-1
Технология OLE Automation 69 Тип (Pascal) Тип (IDL) Описание 01 eVari ant VARIANT Содержит любые данные, тип может Pchar LPSTR меняться динамически. Минимальный размер — 16 байт Указатель на строку, 4 байта PwideChar LPWSTR Указатель па строку, в который для PSafeArray SAFEARRAY храпения каждого символа используют 2 байта. Размер — 4 байта Указатель на массив целых чисел 4 байта Short!nt Single Float 1 байт, целое со знаком, диапазон от -128 до 127 4 байта, с плавающей запятой, диапазон SmallInt Short от 1,5x10 45 до 3,4хЮ38, 7-8 знаков 2 байта, целое со знаком, диапазон SYSINT Int от 32 768 до 32 767 Системная целая переменная со знаком, SYSUINT unsigned int в 32-разрядпых операционных системах совпадает с типом i nteger Системная целая переменная без знака, TdateTime DATE в 32-разрядпых операционных системах совпадает с типом 1 ongword. Другое название переменной этого типа — Cardinal 8 байт, с плавающей запятой, целая Tdecimal DECIMAL часть — число дней, прошедших с 30 декабря 1899 года, дробная часть — доля от 24 часов Структура, содержит число с плавающей WideString BSTR запятой и точность его представления (количество значимых десятичных знаков). Расшифровка содержится в модуле ActiveX, pas Строка переменной длины, для храпения Word unsigned short каждого символа используется 2 байта 2 байта, целое без знака, диапазон WordBool VARIANT_B00L от 0 до 65 535 2 байта, логическая переменная (True = -1, Int64 1nt64 False = 0) 8-байтовое целое число FONTBOLD, FONTBOLD, Передаст параметры шрифта FONTITALIC, FONTNAME, FONTSIZE, FONTSTRIKETHROUGH FONTITALIC, FONTNAME, FONTSIZE, FONTSTRIKETHROUGH продолжение &
70 Глава 1. Основы технологии СОМ Таблица 1.1 (продолжение) Тип (Pascal) Тип (IDL) Описание SCODE SCODE 32-битовос значение статуса, используемое в MAPI Longword unsigned int 4 байта, целое число без знака, диапазон от 0 до 4 294 967 295 Интерфейс IDispatch В технологии СОМ предусмотрена возможность доступа к методам интерфейса при отсутствии информации о порядке реализации методов в виртуальной таб- лице. При реализации интерфейсов среда разработки может сохранить текстовые названия методов в файле, в котором находится скомпилированный код СОМ- сервера. Это означает, что в COM-сервере был реализован интерфейс IDispatch, который является центральным элементом технологии OLE Automation. Ключе- выми методами этого интерфейса являются методы GetldsOfNames и Invoke, кото- рые позволяют клиенту получить у сервера ответ па вопрос, поддерживает ли он метод с указанным именем, а затем, если метод поддерживается, — вызвать его. Подробно реализация и работа интерфейса IDispatch рассматривается в главах 3 и 10, здесь же мы лишь вкратце опишем основной алгоритм вызова методов при помощи интерфейса IDispatch. Когда клиенту требуется вызвать какой-либо метод сервера, сначала он вызы- вает метод GetldsOfNames интерфейса IDispatch, передавая ему имя запрошенного метода сервера. Если сервер поддерживает запрошенный метод, он возвращает его идентификатор — целое число, уникальное для каждого метода. После этого клиент упаковывает параметры в массив переменных типа 01 eVari ant и вызывает метод Invoke, передавая ему массив параметров и идентификатор запрошенного метода. Таким образом, все, что должен знать клиент, — это строковое имя метода. Та- кой алгоритм позволяет работать с наследниками интерфейса IDispatch из язы- ков сценариев. Методы GetTypelnfo и GetTypeInfoCount являются вспомогательными и обеспе- чивают поддержку библиотеки типов объекта. Реализация методов GetldsOfNames и Invoke, предоставляемая СОМ по умолчанию, базируется па библиотеке типов объекта. При работе с интерфейсом IDispatch связь между вызываемыми методами ус- танавливается при выполнении приложения. Такой способ вызова методов ин- терфейсов называют поздним связыванием (late binding). Для подобного вызова методов требуется провести большее число операций, чем при прямом обраще- нии к виртуальной таблице, и это может сказаться па скорости выполнения вы- зовов. При отсутствии библиотеки типов па этапе разработки сервера невоз- можно проверить правильность написания имен методов и списков параметров методов. Это может обнаружиться только па этапе выполнения клиентского приложения — когда произойдет исключение. Поэтому клиентское приложение,
Технология OLE Automation 71 работающее с сервером через интерфейс IDispatch, обязательно следует тестиро- вать на предмет корректного выполнения всех команд. Кроме того, при наличии библиотеки типов для реализации потификациоп- пых сообщений (то есть уведомлений о событиях, которые СОМ-сервер посылает клиенту) в элементах управления ActiveX вывод этих сообщений возможен только через интерфейс IDispatch. Элемент управления ActiveX определяет ин- терфейс для поддержки нотификациоппых сообщений, по сам его не создает. Он должен быть реализован в клиенте, а элемент управления ActiveX должен получить ссылку на него и вызывать методы этого интерфейса в ответ па со- бытия, происходящие с ним. Поскольку СОМ-сервер следует компилировать раньше клиента, пет никакой возможности па этапе компиляции сервера полу- чить доступ к таблице виртуальных методов клиента и связать потификациоп- пые сообщения с методами, определенными в клиенте. Тип данных Variant Delphi обеспечивает поддержку клиентов автоматизации. Тип данных Variant мо- жет содержать ссылку па интерфейс IDispatch и использоваться для вызова его методов. uses ComObj: procedure TForml.ButtonlClick(Sender: TObject): var V : Variant: begin V := CreateOleObject('InternetExpIorer.Application'): V .Tool bar := FALSE: V .Left := (Screen.Width - 600) div 2: V .Width := 600: V .Top := (Screen.Height - 400) div 2; V .Height := 400: V .Visible := TRUE: V.Navigate(URL : = 'file://C:\config.sys'): V .StatusText := V.LocationURL; Sleep(lOOOO): V .Quit: end; Приведенный выше код весьма необычен и заслуживает внимательного рас- смотрения: Я очевидно, что переменная V не обладает ни одним из используемых свойств и методов; я вызываемые свойства и методы нигде не описаны, однако это не ведет к ошибке компиляции; объект создается не по CLSID, а по информативному имени функцией CreateOleObject.
72 Глава 1. Основы технологии СОМ Все это непривычно и выглядит довольно странно. На самом деле — ничего странного пет. Компилятор Delphi просто запоминает в коде программы стро- ковые описания обращений к серверу автоматизации, а па этапе выполнения передает их интерфейсу IDispatch сервера, который и производит синтаксиче- ский анализ и выполнение. Исправим третью строку процедуры следующим образом: V.Leftl := (Screen.Width - 600) div 2; Программа успешно откомпилируется, однако при попытке выполнения вы- даст ошибку с сообщением, что метод Leftl не поддерживается сервером автома- тизации. Такое обращение к серверу, как уже упоминалось, называется поздним свя- зыванием, означающим, что связывание имен свойств и методов объекта с их ко- дом происходит не па этапе компиляции, а на этапе выполнения программы. Достоинства позднего связывания очевидны — не нужна библиотека ти- пов, написание несложных программ упрощается. Столь же очевидны недос- татки — не производится контроль вызовов и передаваемых параметров па этапе компиляции, работает приложение несколько медленнее, чем при раннем связы- вании. ВНИМАНИЕ ---------------------------------------------------------------- Если СОМ-сервср находится в другом апартаменте, временные затраты па позднее связывание пренебрежимо малы по сравнению с затратами па маршалинг вызовов. Разница в скорости между раппим и поздним связыванием становится ощутимой (де- сятки и сотой раз) при нахождении клиента и сервера в одном апартаменте, что воз- можно только для внутрипроцессного сервера при совместимой с клиентом модели потоков. Для впепроцссспого сервера (сервера, размещенного в отдельном исполняе- мом файле) затраты па вызов метода путем раннего и позднего связывания практиче- ски равны. Главным преимуществом раннего связывания является строгий контроль ти- пов па этапе компиляции. Для разрешения проблемы нестрогого контроля типов СОМ предлагает несколько дополнительных возможностей. Диспинтерфейс Диспинтерфейс (dispinterface) — это декларация методов, доступных через ин- терфейс IDispatch. Объявляется диспинтерфейс следующим образом: type IMyDisp = dispinterface ['{EE05DFE2-5549-HDD-9EA9-0020AF3D82DA}' ] property Count: Integer dispid 1 procedure Clear dispid 2: end:
Технология OLE Automation 73 Самих методов может физически и пе существовать (например, они реализу- ются динамически в методе Invoke). Рассмотрим использование диспиптерфейса па простом примере. Объявим диспиптерфейс объекта InternetExplorer и задей- ствуем его в своей программе: type ПЕ = dispinterface ['{0002DF05-0000-0000-С000-000000000046}'] property Visible: WordBool dispid 402: end; procedure TForml.ButtonlClick(Sender: TObject): var IE: HE: begi n IE := CreatedeObjectC' InternetExplorer.Application') as HE: IE.Visible := True: end: Эта программа успешно компилируется и работает, несмотря па то, что в ин- терфейсе объявлено только одно свойство из множества имеющихся свойств и методов. Это происходит благодаря тому, что Delphi пе вызывает методы дис- пиптерфейса напрямую и поэтому пе требует полного описания всех методов в правильном порядке. При вызове метода диспиптерфейса Delphi просто вызы- вает метод Invoke соответствующего метода IDispatch, передавая ему идентифи- катор метода, указанный в параметре dispid. В результате программист полу- чает возможность строго контролировать типы при вызове методов интерфейса IDispatch и вызывать методы, описанные в диспиптерфейсе, без формирования сложных структур данных, требующихся для вызова метода Invoke. Необходимо лишь указать (или импортировать из библиотеки типов сервера) описание дис- пиптерфейса. В описании диспиптерфейса допустимо использовать только OLE-совмести- мые типы данных. Дуальные интерфейсы Идея дуальных интерфейсов (dual interfaces) очень проста. Сервер реализует од- новременно некоторый интерфейс Viable, оформленный по стандартам СОМ, и диспиптерфейс, доступный через интерфейс IDispatch. При этом интерфейс Viable должен наследоваться от IDispatch и иметь идентичный с диспиптер- фейсом набор методов. Такое оформление сервера позволяет клиентам работать с ним наиболее удобным для каждого клиента образом. Клиенты, применяющие интерфейс Viable, вызывают его методы напрямую, а клиенты, использующие позднее связывание, — через методы интерфейса IDispatch. Большинство OLE-серверов реализуют дуальный интерфейс.
74 Глава 1. Основы технологии СОМ Маршалинг и взаимодействие клиента с сервером Маршалинг используется в СОМ для решения проблемы экспорта объектов че- рез адресное пространство процесса. Дальнейшее развитие технологии СОМ, по- лучившее название DCOM (Distributed СОМ), позволило осуществлять взаимо- действие объектов, выполняющихся на разных компьютерах. Как было сказано выше, единственным способом взаимодействия клиента с COM-объектом является использование интерфейсов. На этапе выполнения интерфейс характеризуется адресом, указывающим па другой адрес, который, в свою очередь, указывает па таблицу, содержащую адреса реализации каждой функции, экспортируемой интерфейсом (рис. 1.10). Рис. 1.10. Структура интерфейса Эта структура позволяет обеспечить маршалинг — пересылку указателя между процессами (и в общем случае между компьютерами). Реализация интерфейса COM-объекта заключается в создании подобной струк- туры в памяти и в предоставлении указателя па нее. Весьма существенно, что ад- реса самих методов определяются па этапе выполнения с помощью указателя па интерфейс в момент обращения к ним, а не хранятся в памяти статически. Отме- тим также, что при наличии указателя па метод в таблице виртуальных методов всегда существует и его реализация, поэтому клиенту не требуется пи обрабаты- вать сообщения об обращении к несуществующим функциям, пи иметь собст- венной версии их реализации. Когда клиенту требуется COM-объект, он находит сервер, посылает ему за- прос па создание объекта, а затем получает от пего указатель па исходный интер- фейс, с помощью которого можно получить дополнительные указатели па дру- гие интерфейсы этого объекта. Как было рассказано ранее, местоположение сервера определяется па основа- нии записи в реестре. Затем сервер загружается в оперативную память (если on еще не загружен или если требуется новый экземпляр сервера), создает нужный объект и возвращает клиенту указатель па интерфейс. Если СОМ-сервер является внутрипроцессным, то есть выполненным в виде библиотеки DLL, он загружается в адресное пространство клиента с помощью функции LoadLibrary Win32 API. В этом случае значение указателя па интерфейс непосредственно доступно клиенту (конечно, если и сервер и клиент находятся в одном апартаменте, как показано па рис. 1.11). Если СОМ-сервер является впепроцесспым, СОМ использует функцию CreateProcess, загружая исполняемый файл и инициализируя СОМ-сервер в его
Маршалинг и взаимодействие клиента с сервером 75 адресном пространстве. В этом случае нет возможности передать клиенту значение указателя па интерфейс, так как этот указатель идентифицирует объект, находя- щийся в другом адресном пространстве. Поэтому в адресных пространствах клиента и сервера, как уже было отмечено выше, создаются два объекта: стаб (stub) — представитель клиента в адресном пространстве сервера, имеющий дело с реаль- ным указателем на интерфейс, и прокси (proxy) — представитель сервера в ад- ресном пространстве клиента. Эти два объекта путем маршалинга связываются между собой с целью передачи клиентскому процессу указателя на интерфейс. При этом создается так называемый пакет маршалинга (marshalling packet) — пакет данных, содержащий необходимую информацию для соединения с про- цессом, в котором создан объект. Этот пакет создается с помощью функции CoMarshal Interface COM API, затем он передается процессу клиента любым дос- тупным способом, где другая функция CotlnMarshal Interface превращает этот па- кет в указатель па интерфейс. Стандартный маршалинг осуществляется с помо- щью автоматически генерируемого интерфейса IMarshal (рис. 1.12). Рис. 1.12. Взаимодействие клиента с внепроцессным СОМ-сервером
76 Глава 1. Основы технологии СОМ Сходная технология используется при вызовах удаленных процедур (Remote Procedure Calls, RPC), откуда опа и была заимствована корпорацией Microsoft. Естественно, прокси не содержит реализации методов интерфейса. Все аргу- менты вызываемых методов помещаются в пакет, передаваемый стабу посредст- вом RPC. Прокси распаковывает переданные аргументы, помещает их в стек и обращается к реальному объекту, используя существующий указатель па интер- фейс. Результат выполнения метода упаковывается в пакет и посылается обратно стабу, который распаковывает его и передает клиенту. Если сервер расположен па удаленном компьютере, то при обращении к нему СОМ соединяется со специальным резидентным процессом удаленного компью- тера, контролирующим удаленный запуск служб па нем (наличие такого процесса диктуется обычными соображениями безопасности). Этот процесс осуществляет запуск сервера на удаленном компьютере и возвращает указатель на интерфейс клиентскому компьютеру и клиентскому процессу. В остальном маршалинг осу- ществляется точно так же, как и в случае внепроцесспого сервера, за исключением того, что прокси и стаб, общаясь посредством того же самого механизма RPC, физически находятся пе только в разных процессах, по и на разных компьютерах (рис. 1.13). COM API Delphi, являясь средой быстрой разработки приложений, скрывает от програм- миста тонкости реализации и позволяет ему решать типовые задачи, не вникая детально в COM API. Однако при решении сложных задач полезно знать, какие именно вызовы функций COM API осуществляют функции Delphi, а также уметь делать эти вызовы самостоятельно, В этом разделе мы познакомимся с некоторыми наиболее важными функциями, предоставляемыми СОМ.
COM API 77 Инициализация COM Любой поток, из которого вызываются функции COM API, перед использова- нием этих функций должен произвести инициализацию СОМ. При инициали- зации создается апартамент, в котором в дальнейшем создаются и исполняются объекты. Более подробно об этом написано в главе 6, посвященной моделям потоков СОМ. Инициализация осуществляется вызовом одной из следующих функций: function CoInitialize(pvReserved: Pointer): HResult: stdcall: function CoInitializeEx(pvReserved: Pointer: colnit: Longint): HResult; stdcall: Параметр pvReserved не используется и должен быть равен nil. Параметр colnit может принимать одно из следующих значений: Я COINIT_APARTMENTTHREADED — создается одпопоточпый апартамент (Single-Threaded Apartment, STA), все COM-объекты используют один общий поток, вызовы сериализуются; COINIT_MULTITHREADED — создается многопоточный апартамент (Multi-Threaded Apartment, МТА), COM-объекты используют пул потоков, вызовы не сериа- лизуются и для синхронизации потоков требуется предпринимать специаль- ные меры. Вызов функции Coinitialize эквивалентен следующему вызову: Coin!tializeEx(nil. COINIT_APARTMENTTHREADED); Каждому вызову функции CoInitialize(Ex) должен соответствовать вызов про- цедуры CoUninitial ize, объявленной следующим образом: procedure CoUninitialize: stdcall: Если в программе используется модуль ComObj, то инициализация СОМ для основного потока осуществляется этим модулем автоматически при старте при- ложения. По умолчанию инициализируется ST А. Флаги инициализации можно задать в глобальной переменной CoInitFlags в начале кода проекта: program Projectl; uses Forms, ComObj. ActiveX, Unitl in 'Unitl.pas' {Forml}; {$R *.res} begi n CoInitFlags : = COINIT_MULTITHREADED: Application.Initialize: Appl icati on.CreateForm(TForml, Forml); Application.Run: end.
78 Глава 1. Основы технологии СОМ Если в программе создаются дополнительные потоки, использующие СОМ, то следует позаботиться об инициализации в начале процедуры потока и деини- циализации (вызове процедуры CoUnlnitialize) по его завершении: procedure TThreadUsedCOM.Execute; begi n Colnitializelnil); // Код потока, использующий COM CoUnlnitialize; end: Управление памятью COM предоставляет в распоряжение программиста потокозащищенпый, незави- симый от модуля и языка программирования механизм распределения памяти. Многие функции СОМ распределяют память для возвращаемого результата по- средством этого же механизма, и нужно уметь им пользоваться. Доступ к диспет- черу памяти СОМ осуществляется при помощи интерфейса IMalloc: IMalloc = interface!IUnknown) ['{00000002-0000-OOOO-COOO-000000000046}] function All ос(cb: Long!nt): Pointer: stdcall: function Rea Hoc (pv: Pointer; cb: Longint): Pointer; stdcall: procedure Free(pv: Pointer); stdcall: function GetSizeCpv: Pointer): Longint; stdcall: function DidAlloc(pv: Pointer): Integer: stdcall: procedure HeapMinimize: stdcall: end: Перечисленные ниже методы интерфейса IMalloc позволяют выделять память и управлять ею. function Alloc(cb: Longint): Pointer: Функция Al 1 ос выделяет блок памяти размером cb байт и возвращает указа- тель па пего. В случае неудачи возвращается nil. Память не инициализируется и может содержать случайные значения. Реальный размер выделенной памяти может превышать запрошенное количество байт. function Rea 11oc(pv: Pointer; cb: Longint): Pointer; Функция Realloc изменяет размер ранее выделенного блока памяти pv, де- лая его равным cb, и возвращает указатель па новый блок. Новый блок памяти может располагаться по другому адресу, нежели указан в параметре pv, поэтому в дальнейшем вместо pv надо использовать возвращенное функцией значение. Память не инициализируется, старое содержание не сохраняется. Если увели- чить размер блока не удалось, функция возвращает nil, ио вы можете использо- вать ранее выделенный блок pv. procedure Free(pv: Pointer):
COM API 79 Процедура Free освобождает ранее выделенный блок памяти. После вызова этой процедуры дальнейшее использование блока памяти pv недопустимо. function GetSize(pv: Pointer): Longint: Функция GetSize возвращает размер блока памяти, ранее выделенного мето- дом АП ос или Realloc. function DidAl1oc(pv: Pointer): Integer: Функция DidAlloc определяет, был ли блок памяти выделен этим экземпля- ром интерфейса IMalloc или пет. Функция возвращает одно из следующих значе- ний: И 1 — блок памяти был выделен этим экземпляром интерфейса IMalloc; Я 0 — блок памяти был выделен кем-то другим; Я -1 — пе удается определить, кто выделил память. procedure HeapMinimize: Процедура HeapMinimize сжимает выделенные блоки памяти путем удаления пустых страниц. Рекомендуется время от времени вызывать эту функцию, если приложение интенсивно выделяет память. Для получения ссылки на интерфейс IMalloc используется следующая функция: function CoGetMalloc(dwMemContext: Longint; out mall ос: IMalloc): HResult: stdcall: Параметр dwMemContext зарезервирован и должен быть равен 1. Ниже приведен пример использования интерфейса IMalloc: procedure TForml.ButtonlClick(Sender: TObject); var Malloc: IMalloc: PI: PInteger; begin if CoGetMallocd, Malloc) = S_OK then begin PI := Malloc.Alloc(SizeOf(Integer)); PI* := 8: Caption := Format!'Й W , [Malloc.GetSize(PI). PI*]); Malloc.Free(PI); end; end: Для быстрого доступа к функциям распределения памяти COM API предо- ставляет три дополнительных функции: function CoTaskMemAlloc(cb: Longint): Pointer; stdcall: function CoTaskMemRealloc(pv: Pointer: cb: Longint): Pointer: stdcall: procedure CoTaskMemFreeCpv: Pointer): stdcall:
80 Глава 1. Основы технологии СОМ Эти функции не требуют получения ссылки па интерфейс IMalloc, хотя они и аналогичны соответствующим методам этого интерфейса. procedure TForml.Button2C1ick(Sender: TObject): var PI: PInteger: begi n PI := CoTaskMemAlloc(SizeOf(Integer)): if Assigned(PI) then begin РГ := 8: Caption := Format!'M'. [PIA]): CoTaskMemFree(PI): end; end: Кроме того, эти функции удобны для обмена данными между разными модулями приложения. Можно выделить память в DLL при помощи функции CoTaskMemAlloc и освободить ее в исполняемом файле вызовом процедуры CoTaskMemFree. ВНИМАНИЕ ----------------------------------------------------------------------- Функции CoGetMalloc, CoTaskMemAlloc, CoTaskMemRealloc, CoTaskMemFree и интерфейс IMalloc могут использоваться без инициализации СОМ. Это единственное исключе- ние в COM API. Для отладки приложений, работающих с интерфейсом IMalloc, при помощи следующих функций можно установить «ловушку», отслеживающую его вы- зовы: function CoRegisterMallocSpylmallocSpy: IMallocSpy): HResult: stdeal1: function CoRevokeMa11 ocSpy: HResult; stdcall; Параметр mallocSpy должен содержать ссылку па интерфейс IMallocSpy, методы которого будут вызываться при использовании интерфейса IMalloc. Создание СОМ-объектов За создание COM-объекта отвечает следующая функция: function CoCreateInstance(const clsid: TCLSID: unkOuter: IUnknown; dwClsContext: Longint: const iid: TIID; out pv): HResult; stdcall; Ниже перечислены параметры, передаваемые в функцию. » clsid — CLSID создаваемого СОМ-сервера. unkOuter — используется, если объект создается как часть агрегированного объекта, состоящего из нескольких СОМ-объектов. В этом случае этот пара- метр содержит интерфейс IUnknown агрегированного объекта. В противном случае необходимо передать nil.
COM API 81 dwCl sContext — контекст, в котором создается COM-объект. Допустимые зна- чения: □ SomeUserDefinedFunction()_INPROC_HANDLER — объект расположен удаленно, по в адресное пространство вызывающего процесса загружается библиотека, содержащая его клиентские компоненты (обычно используется для нестан- дартного маршалинга); □ CLSCTX_LOCAL_SERVER — объект расположен в исполняемом файле и выпол- няется в другом процессе па том же компьютере; □ CLSCTX_REMOTE_SERVER — объект расположен удаленно, а его свойства опреде- ляются ключом реестра LocalServer32 или LocalService. И lid — IID интерфейса, запрашиваемого у созданного СОМ-сервера. Pv — переменная, принимающая ссылку па запрошенный интерфейс создан- ного объекта. В качестве примера использования функции CoCreatelnstance рассмотрим реа- лизацию функции CreateComObject в модуле ComObj: function CreateComObject(const ClassID: TGUID): IUnknown: begin 01eCheck(CoCreateInstance(ClassID, nil. CLSCTX_INPROC_SERVER or CLSCTX_LOCAL_SERVER, IUnknown, Result)); end; Как видно, функция CreateComObject Delphi является просто оболочкой, ис- пользующей функцию CoCreatelnstance с наиболее часто встречающимися пара- метрами. Возможна ситуация, когда приложение создает большое количество СОМ- объектов одного и того же типа. В этом случае выгоднее создавать их не функцией CoCreatelnstance, которая каждый раз производит загрузку DLL и создание фаб- рики классов, а один раз создать фабрику классов и, не выгружая DLL, исполь- зовать эту фабрику, пока в пей есть необходимость. Для этого надо получить объект класса (class object) — СОМ-сервер, отвечающий за создание экземпля- ров объектов. Сделать это можно при цомощи следующей функции: function CoGetClassObject(const clsid: TCLSID; dwClsContext: Longint: pvReserved: Pointer: const iid: HID; out pv): HResult; stdcall: Ниже перечислены параметры, передаваемые в функцию. Clsid — CLSID создаваемого СОМ-сервера. dwCl sContext — контекст, в котором создается COM-объект. Параметр означа- ет то же, что и одноименный параметр функции CoCreatelnstance. 8 PvReserved — не используется, можно передать nil. № iid — IID запрашиваемого интерфейса. Допустимые значения — IClassFactory и IClassFactory2. И pv — переменная, принимающая ссылку па запрошенный интерфейс.
82 Глава 1. Основы технологии СОМ Функция возвращает ссылку па интерфейс ICIassFactory или IClassFactory2. Если пе стоит задача лицензирования COM-сервера, то наиболее интересны ме- тоды интерфейса ICIassFactory: ICIassFactory = interface!IUnknown) ['{00000001-0000-OOOO-COOO-000000000046}'] function Createlnstance(const unkOuter: IUnknown; const iid: TIID; out obj): HResult; stdcall; function LockServer(fLock; BOOL): HResult; stdcall; end; Ключевой метод Createlnstance как раз и создает экземпляр требуемого объ- екта. Ниже перечислены параметры этого метода. unkOuter — используется, если объект создается как часть агрегированного объекта. В этом случае этот параметр содержит интерфейс IUnknown агрегиро- ванного объекта. В противном случае необходимо передать ni 1. И iid — IID интерфейса, запрашиваемого у созданного СОМ-сервера. obj — переменная, принимающая ссылку па запрошенный интерфейс создан- ного объекта. Приведенный ниже простой пример иллюстрирует технику работы с методом CoGetCl assObject, а заодно позволяет оцепить выигрыш в быстродействии. В каче- стве СОМ-сервера использовался объект RegExp из библиотеки Microsoft VBScript Regular Expressions. Первая процедура создает 1000 экземпляров сервера традиционным способом: procedure TForml.ButtonlCli ck(Sender: TObj ect); var I: Integer: RE: IRegExp; T: Cardinal: begin T := GetTickCount; for I := 1 to 1000 do begin 01eCheck(CoCreateInstance(CLASS_RegExp. nil, CLSCTX_INPROC_SERVER, IIDJRegExp, RE)); RE := nil: end: Caption := IntToStr(GetTickCount - T); end; Во второй процедуре используется метод CoGetCl assObject: procedure TForml.Button2Click(Sender: TObject); var CF: ICIassFactory: I: Integer; RE: IRegExp; T: Cardinal;
COM API 83 begin T := GetTickCount: 01 eCheck(CoGetClassObject(CLASS_RegExp. CLSCTX_INPROC_SERVER, nil. IClassFactory. CF)): for I := 1 to 1000 do begin 01eCheck(CF.CreateInstance(nil. IID_IRegExp. RE)): RE := nil; end: Caption := IntToStr(GetTickCount - T): end: Особенно заметной разница становится при использовании впепроцесспых серверов в исполняемых файлах. Управление загрузкой модулей СОМ самостоятельно решает проблемы со своевременной загрузкой и выгрузкой модулей, требующихся для работы серверов. Однако в некоторых случаях может оказаться полезным вмешаться в поведение СОМ. Обычно это делают либо в целях повышения производительности (предварительно загрузив все требуемые биб- лиотеки и не допуская их выгрузки), либо, наоборот, в целях экономии ресурсов. Рассмотрим, какие функции COM API предоставляет для решения этих задач. function CoLoadLibrary(pszLibName: POleStr: bAutoFree: BOOL): THandle: stdcall: Функция CoLoadLibrary позволяет загрузить в адресное пространство процесса требуемый модуль. При этом если bAutoFree = False, библиотека не будет авто- матически выгружаться. Если ваше приложение часто создает объекты из этой библиотеки или время создания объекта критично, вы можете предварительно загрузить ее и не дать выгружаться. В этом случае при создании объекта библио- тека будет уже загруженной. procedure CoFreeLibraryChlnst: THandle); stdcall; Процедура CoFreeLibrary выгружает библиотеку, загруженную функцией CoLoadLibrary. Вызов этой процедуры требуется только в том случае, если при за- грузке библиотеки параметр bAutoFree был равен значению False. procedure CoFreeUnusedLibraries: stdcall: Процедура CoFreeUnusedLibraries выгружает все неиспользуемые библиотеки (предварительно вызвав их функцию DllCanUnIoadNow). Если для вашего прило- жения остро стоит задача экономии ресурсов — оно должно периодически вызы- вать эту функцию. Функции внутрипроцессного сервера Следующие четыре функции не реализованы в COM API, поэтому должны быть реализованы в каждом внутрипроцессном сервере. Спецификация СОМ опреде- ляет только их прототипы и действия, которые они должны выполнять. Delphi
84 Глава 1. Основы технологии СОМ автоматически реализует эти функции, однако необходимо понимать, что они де- лают, function DllGetClassObject(const clsid, iid: TGUID; var obj): HResult: stdcall: Функция DI IGetCl assObject вызывается непосредственно после загрузки DLL. Ниже перечислены параметры этой функции. » clsid — CLSID создаваемого СОМ-сервера. в iid — IID запрашиваемого интерфейса. Допустимые значения — IClassFactory и IC1 assFactory2. obj — переменная, принимающая ссылку па запрошенный интерфейс. В дальнейшем клиент создает экземпляры объектов при помощи возвращен- ного этой функцией интерфейса. function DllCanUnIoadNow: HResult; stdcall; Функция DllCanUnIoadNow вызывается перед тем, как выгрузить DLL из памяти. Если функция возвращает значение S_OK — библиотека выгружается, иначе — нет. Функция должна возвращать значение S_OK только в том случае, если не ос- талось ни одного объекта, созданного из этой библиотеки. function DllRegisterServer: HResult; stdcall; function DllUnregisterServer: HResult; stdcall; Пара функций DllRegisterServer и DllUnregisterServer вызывается соответст- венно при регистрации и при отмене регистрации сервера (например, с помощью утилиты Regsvr32). Вся ответственность за действия, предпринимаемые при ре- гистрации, лежит па коде этих функций, СОМ лишь обеспечивает их вызов. Стандартные реализации, предоставляемые Delphi, создают необходимые ключи в реестре, однако вы можете предоставить собственный код, выполняющий не- кую более сложную инициализацию. Пример подобного кода приведен в главе 9. Маршалинг интерфейсов Иногда возникает необходимость передать ссылку па COM-объект в другой по- ток выполнения. Для примера рассмотрим поток, который в цикле сравнивает строки при помощи СОМ-сервера RegExp. Сам СОМ-сервер создается в главном потоке приложения: TMyThread = class(TThread) protected procedure Execute; override; public RE: IRegExp; end; procedure TMyThread.Execute: var
COM API 85 I: Integer; begi n for I ;= 1 to 100000 do RE.Pattern := 'bbbb'; end: В главном потоке создаем экземпляр объекта TMyThread и запускаем его: with TMyThread.Create(TRUE) do begin FreeOnTerminate : = TRUE; Memo := Memol; RE := Self.RE; // Ссылка на ранее созданный СОМ-сервер Resume; end; На первый взгляд код кажется работоспособным и даже работает. Однако то, что он будет работать в любых условиях, не гарантируется. Например, вставим после запуска потока следующий код и запустим приложение снова: with ТМуThread.Create(TRUE) do begin FreeOnTerminate : = True; RE := Self.RE; Resume; end: for I := 1 to 1000 do begin RE.Pattern : = 'aaaa'; Sleep(O); // Отдали квант времени другому потоку if RE.Test('aaaa') then S := 'TRUE' el se S := 'FALSE'; Memol.Lines.Add(S); Appl i cation.ProcessMessages; end; В многострочном поле Memol появятся беспорядочно чередующиеся строки TRUE и FALSE. Почему? Ведь мы пе создавали явно МТА, и обращения к СОМ-сер- веру должны были упорядочиваться по сообщениям Windows, то есть по вызо- вам ProcessMessages в главном потоке. Все очень просто — мы нарушили правила СОМ, просто передав интерфейс в другой поток, по пе создав в нем соответст- вующего прокси. В пашем случае это привело лишь к некорректной работе при- ложения, в других — может вызывать ошибки доступа к памяти и т. п. Для кор- ректной передачи интерфейса в другой поток выполнения СОМ предоставляет специальные функции. Мы должны записать интерфейс в поток данных IStream, передать ссылку па пего в другой поток выполнения и там преобразовать поток данных IStream в интерфейсную ссылку. Для записи интерфейса в поток данных служит следующая функция: function CoMarshalInterThreadInterfaceInStream(const iid: TIID; unk: IUnknown; out stm: IStream): HResult; stdcall;
86 Глава 1. Основы технологии СОМ Ниже перечислены параметры функции. Ж iid — IID записываемого интерфейса. И unk — ссылка иа интерфейс. S stm — адрес переменной, в которую будет возвращена ссылка па интерфейс IStream. Полученный адрес переменной, в которую будет возвращена ссылка па ин- терфейс IStream, передается в другой поток данных, который получает из него интерфейс путем вызова следующей функции: function CoGetInterfaceAndReleaseStream(stm: IStream: const iid: TIID: out pv): HResult: stdcall: Ниже перечислены параметры функции. в stm — ссылка па полученный ранее интерфейс IStream. в iid — IID получаемого интерфейса. S. pv — переменная, в которую будет записан полученный интерфейс. Функция всегда удаляет переданный ей интерфейс IStream, поэтому нам при- дется принять меры, чтобы среда Delphi не пыталась удалить его повторно. Таким образом, мы должны модифицировать приведенный ранее код. В глав- ном потоке выполнения интерфейс RE сохраняется в потоке данных IStream: procedure TForml.ButtonlClickCSender: TObject): var Str: IStream: S: String: I: Integer: begin 01 eCheck(CoMa rshalInterThreadInterfaceInStream(11D_IRegExp, RE. Str)): with TMyThread.Create(TRUE) do begin FreeOnTerminate := True: ST := Str; Resume; end: for I := 1 to 1000 do begin RE.Pattern := 'aaaa': Sleep(O): if RE.Test!'aaaa') then S := 'TRUE' el se S := 'FALSE': Memol.Lines.Add(S): Appl i cation.ProcessMessages: end: end:
COM API 87 В потоке выполнения TMyThread мы получаем интерфейс RE из потока данных IStream и обнуляем ссылку па интерфейс IStream. Перед обнулением ссылка при- водится к указателю, чтобы пе вызвался метод Release: TMyThread = class(TThread) private RE: IRegExp: protected procedure Execute; override: public St: IStream: end: procedure TMyThread.Execute; var I: Integer; begin CoInitialize(nil); // Инициализация COM в потоке try 01 eCheck(CoGetInterfaceAndReleaseStream(ST. IID_IRegExp. RE)); Pointer(ST) := nil; for I := 1 to 100000 do RE.Pattern : = 'bbbb'; finally Collninitialize: end; end; На этот раз все работает корректно и многострочное поле Memol заполняется строками TRUE. Работа с идентификаторами GUID СОМ предоставляет богатый набор функций по работе с идентификаторами сер- веров и GUID. Наиболее важной является функция создания нового идентифи- катора GUID: function CoCreateGuid(out guid: TGUID): HResult: stdcall; Эта функция вызывает функцию UuidCreate. В версиях Windows, вышедших до Windows 2000, генерируемый идентификатор GUID содержал МАС-адрес се- тевого адаптера компьютера, что позволяло определить, па каком компьютере он сгенерирован. В Windows 2000 и последующих версиях алгоритм генерации из- менен, и результат больше пе позволяет определить источник происхождения GUID. Если необходимо сгенерировать GUID по старому алгоритму, можно воспользоваться функцией UuidCreateSequential.
88 Глава 1. Основы технологии СОМ Часто возникает задача по программному идентификатору (строке вида IntennetExplonen.Application) узнать CLSID соответствующего СОМ-сервера или наоборот, но идентификатору CLSID узнать его программный идентификатор (ProgID). Эту задачу решает пара функций: function CLSIDFnomPnoglDCpszProglD: POleStn; out clsid: TCLSID): HResult: stdcall; function PnoglDFnomCLSIDIconst clsid: TCLSID; out pszPnogID: POleStn): HResult: stdcall: Если соответствующий идентификатор ProgID или CLSID не зарегистриро- ван в системе, функции возвращают код ошибки. Функция PnoglDFnomCLSID выде- ляет память под строку pszPnogID, и вызывающее приложение должно освобо- дить ее путем вызова функции CoTas kMemFnee. Для преобразования строки в GUID служит следующая пара функций: function CLSIDFnomStringtpsz: POleStn; out clsid: TCLSID): HResult; stdcall: function IIDFnomStninglpsz: POleStn: out iid: TIID): HResult: stdcall; Следующие две функции выполняют обратное преобразование: function StningFnomCLSID(const clsid: TCLSID; out psz: POleStn): HResult: stdcall: function StningFnomllDIconst iid: TIID: out psz: POleStn): HResult: stdcall: Обе эти функции выделяют память под результирующую строку, и вы должны сами освободить ее, вызвав функцию CoTaskMemFnee. Если мы хотим выделить буфер памяти самостоятельно, нам следует использовать следующую функцию: function StningFnomGUID2(const guid: TGUID: psz: POleStn; cbMax: Integen): Integen; stdcall: Параметр cbMax передает в функцию размер буфера. Функция возвращает О, если размер буфера недостаточен, или количество заполненных байт в буфере, включая завершающие символы #0. Для сравнения идентификаторов GUID можно использовать любую из сле- дующих трех функций: function IsEqualGUIDIconst guidl. guid2: TGUID): Boolean; stdcal1: function IsEqualIID(const iidl, iid2: TIID): Boolean; stdcall; function IsEqualCLSIDIconst clsidl, clsid2: TCLSID): Boolean; stdcal1; Все три функции возвращают Tnue, если переданные идентификаторы GUID одинаковы, и False в противном случае. Основные приемы работы с описанными в этом разделе функциями иллюст- рирует следующий пример:
Заключение 89 procedure TForml.ButtonlClick(Sender: TObject): var G. Gl: TGUID; W: PWideChar; I: Integer: begi n 01eCheck(CLSIDFroniProgID(' InternetExplorer.Application', G)): 01eCheck(StringFromCLSID(G, W)): Memol.Lines.Add(W); CoTaskMemFree(W): W := CoTaskMemAlloc!100 * SizeOf(WideChar)); StringFromGUID2(G. W. 100); Memol.Lines.Add(W): CoTaskMemFree(W); 01eCheck(ProgIDFromCLSID(G. W)): Memol.Lines.Add(W): CoTaskMemFree(W): 01 eCheck(IIDF romSt ring( '{0002DF01-0000-0000-C000-111111111111}'. G)): 01eCheck(StringFromIID(G. W)): Memol.Lines.Add(W): CoTaskMemFree(W): for I := 0 to 9 do begin 01eCheck(CoCreateGuid(G)): 01eCheck(StringFromIID(G, W)): Memol.Lines.Add(W): CoTaskMemFree(W): end: 01 eCheck(11DFromSt ring( '{0002DF01-0000-0000-C000-llllllllllll}', Gl)): if IsEqualGUIDIG, Gl) then Memol.Lines.Add('EQUAL') el se Memol.Lines.Add('NOT EQUAL'): if IsEqualGUID(G. G) then Memol.Lines.Add('EQUAL') el se Memol.Lines.Add('NOT EQUAL') end; Заключение В этой главе мы обсудили основы технологии СОМ. Были рассмотрены зада- чи, которые требуется решать при взаимодействии объектов, созданных в разных модулях и приложениях, такие как передача параметров методов, использова- ние общих областей памяти, разные форматы храпения данных в разных языках
90 Глава 1. Основы технологии СОМ программирования и т. д. Мы обсудили, каким образом эти задачи решаются в СОМ. В частности, познакомившись с понятием интерфейса, мы узнали, что: 8 интерфейс — это не класс; 8 интерфейс строго типизирован; в интерфейс является неизменным контрактом. Рассмотрев интерфейс IUnknown, мы выяснили, как реализуется автоматиче- ское управление памятью и подсчет ссылок. Мы узнали, что этот интерфейс яв- ляется предком для всех интерфейсов и на его уровне реализовано корректное решение проблемы резервирования и освобождения ресурсов операционной сис- темы. Далее мы рассмотрели реализацию интерфейсов и использование их внутри приложений. Мы узнали, что представляют собой COM-серверы, как осуществляется в них передача интерфейсов и параметров, как поддерживаются в Delphi стандартные интерфейсы СОМ. Мы рассмотрели интерфейсы ITypeLib и ITypelnfo, отвечаю- щие за информирование разработчиков клиентских приложений об интерфей- сах, их GUID, списке и параметрах поддерживаемых методов, а также примене- ние для этой цели библиотек типов и языка IDL. Мы также изучили вопросы создания COM-серверов с библиотеками типов и без таковых, а также вопросы создания СОМ-клиентов. Мы познакомились с технологией OLE Automation, интерфейсом IDispatch, позволяющим вызывать методы сервера «по имени», а также с применением дис- пинтерфейсов. Мы также рассмотрели интерфейс IMarshal и взаимодействие СОМ- клиентов с внутрипроцессными, внепроцессными и удаленными СОМ-серверами. Мы обсудили основные функции COM API, которые часто используются при создании COM-серверов и СОМ-клиентов. Обсудив основные основы технологии СОМ, мы можем перейти к изучению вопросов создания конкретных типов COM-серверов и соответствующих техноло- гий. Первым типом таких COM-серверов будут элементы управления ActiveX, которым посвящена следующая глава.
ГЛАВА 2 Создание элементов управления ActiveX Технология ActiveX, рассматриваемая в данной главе, базируется на технологии Microsoft СОМ и позволяет создавать и использовать программные компоненты, предоставляющие различные сервисы другим приложениям и операционной сис- теме. С помощью технологии ActiveX можно создавать приложения, собираемые из готовых компонентов — элементов управления ActiveX. При этом не имеет значения, па каком языке программирования написаны готовые компоненты и ис- пользующее их приложение — лишь бы средство разработки поддерживало воз- можность использования таких компонентов в разрабатываемом приложении (обычно называемом контейнером). Элементы управления ActiveX своим поведением напоминают компоненты Delphi. Их можно поместить па проектируемую форму, и при этом в инспекторе объектов становятся доступными их свойства и события. Можно также вызывать их методы па этапе выполнения использующего их приложения. Главное отличие элементов управления ActiveX от компонентов VCL заключается в том, что если компоненты, написанные па Delphi, доступны только в Delphi и C++Builder, то элементы управления ActiveX можно использовать в любых средствах разработки, допускающих применение в приложениях СОМ-объектов, включая Microsoft Visual Basic, Microsoft Visual C++, Sybase PowerBuilder, средства разработки для платформы Microsoft .NET и др. Элементы управления ActiveX представляют собой библиотеки, содержащие исполняемый код. Эти библиотеки могут быть использованы в различных при- ложениях как встроенные элементы, поэтому они обладают свойствами, собы- тиями и методами, доступными через механизм автоматизации. Подавляющее большинство современных средств разработки, как правило, позволяет включать такие элементы в создаваемые с их помощью приложения. Помимо этого, эле- менты управления ActiveX нередко используются в качестве расширений web- браузеров с целью придания им дополнительной функциональности, например для отображения документов, не поддерживаемых данным браузером. Отметим, что элементы управления ActiveX представляют собой внутрипро- цессные серверы, выполняющиеся в адресном пространстве приложения. Как любой СОМ-сервер, элемент управления ActiveX обладает уникальным идентификатором GUID и должен быть зарегистрирован в реестре. На основа- нии этой записи может быть осуществлен поиск местоположения файла с рас- ширением *.осх, содержащего его реализацию.
92 Глава 2. Создание элементов управления ActiveX Таким образом, создав элемент управления ActiveX, обладающий интересую- щей пас функциональностью, мы можем дать возможность его будущим пользо- вателям встраивать этот элемент в свои приложения (например, написанные па Visual Basic, PowerBuilder, Delphi, C++Builder и др.), отображать его в web-брау- зере в составе выгруженной с нашего web-сервера HTML-страницы, включать его в состав документов Microsoft Office, при этом мы не обязаны предоставлять исходный текст этого компонента Когда следует создавать элементы управления ActiveX? Естественно, в тех слу- чаях, когда функциональность, содержащаяся в таком элементе, уникальна. Нет смысла создавать элемент управления ActiveX, реализующий функциональность кнопки или текстового поля — таких элементов управления, в том числе выпол- ненных в виде ActiveX, па рынке готовых компонентов более чем достаточно. Нет смысла также создавать элемент ActiveX, если он понадобится только в Delphi, — в этом случае проще создать VCL-компонент, который будет работать в использую- щем его приложении значительно быстрее, чем аналогичный элемент ActiveX. Но создание элемента управления, реализующего, к примеру, часть автоматизиро- ванного рабочего места какой-либо категории сотрудников вашего предприятия, может оказаться весьма полезным, особенно в перечисленных ниже случаях. 8 При применении на предприятии нескольких различных инструментальных средств, например Delphi и Visual Basic. В этом случае разработчик, исполь- зующий Visual Basic, может встраивать в свои приложения элементы управле- ния ActiveX, созданные другими разработчиками и реализующими какую-либо функциональность, необходимую для нескольких различных приложений. В При широком использовании технологий и протоколов Интернета при созда- нии корпоративных решений. В этом случае элемент управления ActiveX, реализующий подобную функциональность, может быть встроен в HTML- страницу и отображен в web-браузере. Такой подход существенно облегчает решение проблемы обновления версий автоматизированных рабочих мест, так как вместо установки новых версий па рабочих станциях достаточно заменить один экземпляр элемента управления ActiveX, хранящийся на web- сервере. Наиболее характерным случаем применения такого подхода может быть выполненный в виде ActiveX тонкий, или облегченный, клиент (thin client), получающий данные от удаленного сервера приложений, являющегося, в свою очередь, клиентом серверной СУБД. Ж Нередко элементы управления ActiveX входят в состав различных SDK (Soft- ware Development Kit — пакет разработки программ) — наборов библиотек и утилит, поставляемых с некоторыми видами аппаратного и программного обеспечения (в качестве примеров аппаратного обеспечения можно привести некоторые разновидности видеокамер и сканеров, а в качестве примеров про- граммного обеспечения — некоторые геоипформациоппые системы, средства трехмерной графики, генераторы отчетов, средства аналитической обработки данных). Наличие элементов управления ActiveX в составе SDK таких продук- тов позволяет создавать решения, использующие оборудование или программ- ное обеспечение без затрат, связанных с разработкой части пользовательского интерфейса, относящейся непосредственно к функциональности данного про- дукта (например, с реализацией вывода изображения с видеокамеры или ото-
Создание элементов управления ActiveX на основе VCL-компонентов 93 бражения карт в геопнформационной системе). Нередко один и тот же элемент управления ActiveX входит в состав SDK и используется в самом продукте или сопровождающем его программном обеспечении (например, в утилитах для работы с камерой или в самой геоинформационной системе). Такой подход к планированию и созданию архитектуры программного обеспечения позволяет сократить затраты на его разработку и дальнейшую модификацию. Описанные достоинства сделали технологию ActiveX за последние два года весьма популярной, и именно поэтому многие современные средства разработки, такие как Delphi, позволяют создавать элементы управления ActiveX. Эти сред- ства обычно имеют встроенные механизмы поддержки спецификации ActiveX, действующие путем автоматической генерации соответствующего кода (хотя, конечно, не возбраняется писать подобный код вручную). Спецификация ActiveX представляет собой набор правил (а именно, описа- ние стандартных интерфейсов), с помощью которых следует создавать такие эле- менты управления. Отметим, что текущая версия этой спецификации учитывает возможность использования в качестве контейнера web-браузеров и загрузки элементов ActiveX с удаленных web-серверов с их автоматической регистрацией. Создание элементов управления ActiveX на основе VCL-компонентов Начиная с версии 3, Delphi позволяет создавать элементы управления ActiveX на основе VCL-компонентов. При этом могут использоваться не только компо- ненты, поставляемые с Delphi, но и созданные программистом или приобретен- ные у сторонних производителей. Процесс создания элемента управления ActiveX весьма прост. Для этого сле- дует открыть окно репозитария объектов и на странице ActiveX выбрать значок ActiveX Library (рис. 2.1). Рис. 2.1. Страница ActiveX репозитария объектов
94 Глава 2. Создание элементов управления ActiveX После того как будет сгенерирована «пустая» библиотека, следует добавить к ней элемент управления ActiveX, выбрав значок ActiveX Control на той же стра- нице репозитария. Далее следует заполнить появившееся окно мастера создания элементов управления ActiveX (рис. 2.2). Рис. 2.2. Выбор имени элемента управления ActiveX, имен модулей и базового VCL-класса В раскрывающемся списке VCL Class Name диалогового окна ActiveX Control Wizard следует выбрать VCL-компонент, на основе которого будет создан эле- мент ActiveX. В качестве примера выберем компонент TCalendar. Отметим, что при установке флажка Make Control Licensed автоматически бу- дет сгенерирован файл с расширением *.lic, без которого данный элемент управ- ления ActiveX нельзя будет использовать ни в одном из средств разработки, но можно будет поставлять с готовыми приложениями. Это удобно в случае, когда элемент управления ActiveX поставляется бесплатно его автором в составе гото- вого продукта, но требует отдельного лицензирования при встраивании его дру- гими пользователями в свои разработки. Флажок Include Version Information нужен для того, чтобы информация о версии элемента управления ActiveX была доступна применяющим его приложениям. Если создаваемый элемент планируется использовать в Visual Basic 4.0, следует установить этот флажок. В результате работы мастера будут созданы несколько модулей, сгенерирован идентификатор GUID будущего элемента управления ActiveX, а также соответст- вующая библиотека типов, содержащая сведения о свойствах, событиях и методах компонента ActiveX, как характерных для всех элементов управления ActiveX, так и унаследованных от исходного VCL-компонента (рис. 2.3). В коде, связанном с реализацией ActiveX, можно найти описание этих свойств и методов. Отметим, что для включения элемента управления ActiveX в приложение необходимо, чтобы это приложение импортировало его библиотеку типов —
Создание элементов управления ActiveX на основе VCL-компонентов 95 элементы управления ActiveX, в отличие от некоторых других СОМ-серве- ров, обязательно должны экспонировать (то есть предоставлять потенциаль- ным клиентам) свою библиотеку типов. После этого па форме, па которую по- мещается элемент ActiveX, создается контейнер, представляющий собой СОМ- объект. Рис. 2.3. Библиотека типов созданного элемента ActiveX В элементе ActiveX содержатся интерфейс IDispatch и диспиптерфейс. Ин- терфейс IDispatch предоставляет свойства и методы, которые экспонируются элементом управления ActiveX. Ссылка па интерфейс IDispatch передается кон- тейнеру, и контейнер может изменять свойства и вызывать методы элемента управления ActiveX. Кроме того, контейнер считывает IID (Interface Identifier — идентификатор интерфейса) и список методов диспиптерфейса элемента ActiveX. Далее внутри контейнера создается интерфейс IDispatch с тем же самым иден- тификатором IID и тем же самым списком методов. Ссылка па этот интерфейс передается элементу ActiveX. При этом внутри самого элемента ActiveX ника- ких дополнительных диспинтерфейсов пе создается. Полученная ссылка на интерфейс IDispatch элемента ActiveX используется инспектором объектов для вызова методов, устанавливающих значения свойств элемента ActiveX. Обработчики событий, доступные в инспекторе объектов (или в его аналоге в других средствах разработки), связываются с интерфейсом IDispatch контейнера (рис. 2.4). Если па этапе выполнения необходимо изменить свойства элемента ActiveX или выполнить его методы, контейнер обращается к интерфейсу IDispatch эле-
96 Глава 2. Создание элементов управления ActiveX мента ActiveX. И, наоборот, при возникновении событий, связанных с элемен- том управления ActiveX, он обращается к интерфейсу IDispatch контейнера. ActiveX Приложение Рис. 2.4. Взаимодействие элемента ActiveX с контейнером Как будет показано в дальнейшем, методы COM-объекта можно вызвать либо через виртуальную таблицу методов (раннее связывание), либо через интер- фейс IDispatch (позднее связывание). Для вызова методов элемента управления ActiveX и изменения его свойств контейнером требуется раннее связывание. Од- нако при возникновении событий элемент управления ActiveX вызывает методы контейнера только через интерфейс IDispatch. Причина такого различия очевид- на — элемент управления ActiveX всегда разрабатывается раньше контейнера, в котором он используется, и при его разработке таблица виртуальных методов контейнера недоступна. Далее следует сохранить и скомпилировать проект, а затем зарегистрировать элемент ActiveX в реестре (рис. 2.5). Это делается выбором команды Run ► Register ActiveX Server. Рис. 2.5. Сообщение о регистрации элемента ActiveX в реестре Windows После регистрации можно протестировать созданный элемент управления ActiveX, открыв его, например, в Visual Basic 6. Отметим, что версии 5 и 6 именно
Создание элементов управления ActiveX на основе VCL-компонентов 97 этого средства разработки широко используют элементы управления ActiveX в качестве составных частей создаваемых с их помощью приложений. Фактиче- ски приложения Visual Basic 5 и 6 собраны целиком из элементов управления ActiveX. Более того, спецификация ActiveX создана в предположении, что кон- тейнерами для этих элементов управления в первую очередь могут быть Visual Basic и Visual C++ (и лишь затем остальные средства разработки). Поэтому удачное тестирование поведения ActiveX в Visual Basic может в какой-то сте- пени гарантировать, что в других средствах разработки, в том числе и в Visual Studio .NET, этот элемент будет вести себя точно так же. При отсутствии Visual Basic можно воспользоваться и более широко распро- страненным инструментом Visual Basic for Applications. С этой целью можно за- пустить Microsoft Excel или Microsoft Word, создать новый документ, вывести на экран панель инструментов Visual Basic и щелкнуть на кнопке Visual Basic Editor. Далее следует выбрать в окне Project имя вновь созданного документа, щелкнуть на нем правой кнопкой мыши и в открывшемся контекстном меню вы- брать команду Insert ► User Form. На экране появится редактор форм Visual Basic и панель элементов (toolbox). Далее нужно щелкнуть правой кнопкой мыши на панели элементов и выбрать в контекстном меню команду Additional Controls. В списке всех зарегистрированных элементов управления ActiveX открывшегося окна (рис. 2.6) следует выбрать нужный, и он автоматически окажется на панели элементов Visual Basic (можно поместить его на единственную имеющуюся там страницу элементов управления или создать новую). Рис. 2.6. Добавление своего элемента управления ActiveX в панель элементов Visual Basic for Applications После этого можно поместить созданный нами элемент управления ActiveX на форму (рис. 2.7) и попытаться изменить какие-либо его свойства, используя для этой цели окно Properties.
98 Глава 2. Создание элементов управления ActiveX Рис. 2.7. Тестирование элемента управления ActiveX в Visual Basic for Applications И, наконец, можно вернуться в редактируемый документ, поместить на пего кнопку, дважды щелкнуть па пей и в окне редактора кода создать процедуру, пока- зывающую созданную форму, вписав в сгенерированный код строку UserForml. Show: Private Sub CommandButtonl_Click() UserForml.Show End Sub Теперь можно щелкнуть на кнопке Exit Design Mode панели инструментов Visual Basic. После этого щелчок на созданной в теле документа кнопке приведет к появлению диалогового окна с созданным нами элементом управления. Можно было бы, конечно, протестировать поведение созданного элемента ActiveX, установив его в палитру компонентов Delphi, по это не самый лучший способ тестирования — ведь в основе нашего элемента ActiveX лежит та же са- мая библиотека VCL, что и в основе создаваемого приложения для тестирования ActiveX. Использование для этой цели любого средства разработки, не имеющего отношения к VCL и способного работать с элементами управления ActiveX в соз- даваемых приложениях, более оправдано. При этом следует заметить, что Visual
Создание страниц свойств 99 Basic for Applications представляет собой наиболее часто встречающееся средство разработки такого класса, так как входит в состав самого популярного в пашей стране офисного пакета. Создание страниц свойств Так как элементы управления ActiveX могут применяться в средствах разработки, нередко они обладают набором страниц свойств, позволяющим пользователям менять те или иные свойства элементов управления ActiveX, используя для этого более широкий спектр интерфейсных элементов, нежели предоставляемый ин- спектором объектов Delphi пли его аналогами в других средствах разработки. Страницы свойств немного напоминают окна редакторов свойств некоторых VCL- компопептов. Для создания страницы свойств выберем значок Property Page па странице ActiveX репозитария объектов. В результате появится форма, па которой можно размещать интерфейсные элементы. Создадим страницу для редактирования свойств GridLineWidth, Day, Month, Year. Для этого разместим па вновь созданной форме два компонента TStaticText и два компонента TEdit (рис. 2.8). Рис. 2.8. Страница свойств на этапе разработки В страницах свойств предпочтительно использовать именно компонент TStaticText, а не TLabel, так как последний не обладает дескриптором окна (win- dow handle) и, следовательно, при его применении работа связанных с ним «горя- чих» клавиш может оказаться некорректной. В созданной форме имеются сгене- рированные прототипы обработчиков событий UpdatePropertyPage и UpdateObject. Добавим в них код для изменения свойств элемента управления ActiveX при ре- дактировании данных в интерфейсных элементах страницы свойств и синхрони- зации значений в интерфейсных элементах с соответствующими свойствами элемента управления ActiveX: procedure TPropertyPagel.UpdatePropertyPage: begin { Update your controls from OleObject } Editl.Text := OleObject.GridLineWidth: Edit2.Text := 01 eObject.Day;
100 Глава 2. Создание элементов управления ActiveX Edit3.Text ; = 01 eObject.Month; Edit4.Text ;= OleObject.Year; end; procedure TPropertyPagel.UpdateObject; begin { Update OleObject from your controls } 01 eObject.GridLineWidth := Editl.Text: 01 eObject.Day : = Edit2.Text; 01 eObject.Month : = EditS.Text; OleObject.Year := Edit4.Text; end; Возможность обращения к методам и свойствам OLE-объектов, неизвестным заранее, обусловлена тем, что названия этих свойств и методов рассматриваются компилятором Delphi как символьные строки, которые будут переданы в методе Invoke интерфейса IDispatch. Во многих случаях это весьма удобно (в C++, на- пример, такая возможность пе предусмотрена стандартами языка, и поэтому для использования подобного упрощенного синтаксиса требуется описание соответст- вующих классов и их методов или вызов специально предназначенных для этой цели функций). Далее следует создать ссылку на странице свойств в модуле, описывающем реализацию элемента ActiveX. Модификация кода этого модуля заключается во вставке в сгенерированную процедуру DefinePropertyPages строки, указывающей на необходимость регистрации страницы свойств; procedure TCalendarX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage); begin { TODO: Define property pages here. Property pages are defined by calling Def 1 nePropertyPage with the class Id of the page. For example. De fl nePropertyPage(С1ass_Ca1endarXPage); } DefinePropertyPage(Class_PropertyPagel); end; Далее следует заново скомпилировать и снова зарегистрировать библиотеку ActiveX. Если теперь в среде разработки Visual Basic поместить па пользовательскую форму наш элемент ActiveX (возможно, при этом потребуется удалить его с на- пели элементов и снова установить на нее), выделить его и щелкнуть па кнопке с многоточием в строке Custom окна редактора свойств, мы увидим созданную нами страницу (рис. 2.9). Можно убедиться, что при изменении значений в полях ввода изменяются и соответствующие свойства элемента управления ActiveX (рис. 2.10).
Создание страниц свойств 101 Рис. 2.9. Страница свойств на этапе тестирования элемента управления ActiveX Рис. 2.10. Результат применения страницы свойств Отметим, что страницы свойств широко применяются при создании элемен- тов управления ActiveX, предназначенных не для готовых продуктов, а именно для сред разработки, например, элементов управления ActiveX, входящих в со- став различных SDK.
102 Глава 2. Создание элементов управления ActiveX Создание активных форм Активная форма — это элемент управления ActiveX, содержащий несколько VCL-компонентов. Благодаря активным формам существенно расширяется круг доступных для элементов ActiveX функциональных возможностей. Процесс соз- дания такого элемента ActiveX происходит примерно так же, как создание обыч- ного приложения. Рассмотрим простейший пример создания активной формы. Для этого сле- дует выбрать значок Active Form на странице ActiveX репозитария объектов. Далее потребуется ответить на вопросы об имени вновь создаваемого элемента ActiveX, после чего в дизайнере форм появится пустая форма — заготовка будущего элемента управления ActiveX. Добавим на эту форму компоненты TCheckBox, TButton, TImage и ТОрегРзctureDi al од, изменив некоторые из их свойств (рис. 2.11). Рис. 2.11. Активная форма на этапе разработки Создадим обработчики событий, связанные со щелчками мыши на компонен- тах TCheckBox и TButton: procedure TActiveFormX.ButtonlClick(Sender: TObject): begin if OpenPictureDialogl.Execute then Imagel.Pi cture.LoadFromFi1e(0penP1ctureDi alogl.Fi1 eName): end: procedure TActiveFormX. CheckBoxlClick(Sender: TObject): begin Imagel.Stretch := CheckBoxl.Checked: end: Теперь можно скомпилировать приложение, зарегистрировать созданный эле- мент ActiveX и протестировать его указанным выше способом (рис. 2.12).
Создание активных форм 103 Рис. 2.12. Тестирование активной формы в Visual Basic for Applications Можно также протестировать созданный и зарегистрированный элемент ActiveX, открыв его в Internet Explorer. Для этой цели можно выбрать в меню Delphi команду Project ► Web Deployment Options и, если в локальной сети отсут- ствуют web-серверы, в полях Target dir, Target URL, HTML dir на странице Project открывшегося диалогового окна указать имя какого-нибудь локального каталога (рис. 2.13). Затем можно выбрать команду Project ► Web Deploy и по окончании работы мастера Web Deployment Wizard открыть в Internet Explorer автоматически сгене- рированную Delphi HTML-страницу с именем, совпадающим с именем создан- ного проекта (рис. 2.14). Отметим, что для успешного отображения элемента управления ActiveX в брау- зере требуется браузер Microsoft Internet Explorer версии 3.0 и выше, при этом в общем случае (когда элемент ActiveX не зарегистрирован) настройки уровня безопасности должны позволять загрузку и выполнение элементов ActiveX, распо- ложенных в зоне местной интрасети. Эти настройки с точки зрения безопасности существенно отличаются от установок, принятых по умолчанию, — ведь ActiveX
104 Глава 2. Создание элементов управления ActiveX содержит исполняемый код, который будет выполняться в адресном пространстве приложения Internet Explorer, запущенного на компьютере пользователя. Вопросы безопасности, связанные с отображением элементов управления ActiveX в Internet Explorer, более подробно будут освещаться далее в этой главе. Рис. 2.13. Диалоговое окно Web Deployment Options Рис. 2.14. Тестирование активной формы в Internet Explorer
Создание активных форм 105 Если для отображения страниц, содержащих элементы управления ActiveX, используется какой-либо другой браузер, например Netscape Communicator, он должен быть оснащен модулем расширения, позволяющим интерпретировать тег <OBJECT> языка HTML как элемент управления ActiveX (естественно, такая воз- можность существует только для браузеров, выполняющихся под управлением 32-разрядных версий Windows). Отметим также, что сгенерированную автоматически страницу можно в даль- нейшем отредактировать с помощью любого HTML-редактора. При поставке элементов управления ActiveX через Интернет или при исполь- зовании их в интрасетях процедура аналогична описанной, но вместо имен локаль- ных каталогов в поле Target URL окна Web Deployment Options следует указать ад- рес web-сервера (рис. 2.15). Рис. 2.15. Настройка параметров поставки ActiveX через Интернет Следует обратить внимание на следующее: значения в полях Target dir и Target URL представляют собой путь к одному и тому же каталогу, но в первом случае путь задается с точки зрения владельца локального компьютера, а во втором — с точки зрения пользователя-гостя, обращающегося к web-серверу через Интер- нет. Конкретное соответствие определяется в настройках web-сервера, и перед началом заполнения данного диалогового окна необходимо их уточнить. Ката- лог, в который помещают файл с расширением *.осх или *.cab, должен быть дос- тупен для чтения пользователям-гостям, равно как и каталог, указанный в поле HTML dir — в него будет помещен сгенерированный Delphi простейший HTML- файл, содержащий ссылку на элемент управления ActiveX. При установке Micro- soft Internet Information Services таким каталогом по умолчанию считается ката-
106 Глава 2. Создание элементов управления ActiveX лог C:\lnetpub\WWWRoot. Соответственно, URL-адрес, который определен для каждого web-сервера и указывает па этот каталог, — это ййр://<имя компьютера:*, если тестирование будет проводиться па компьютере, находящемся в локаль- ной сети. Отметим, что при загрузке элементов управления ActiveX с web-серверов настройки уровня безопасности должны позволять загрузку и выполнение эле- ментов ActiveX, расположенных на данном web-сервере. Помимо этого следует обратить внимание па дополнительные «пакеты» или другие файлы, которые следует включить в поставку, если параметры проекта требуют использования каких-либо дополнительных библиотек. Разделение эле- мента управления ActiveX на несколько файлов и выделение отдельных «паке- тов» может потребоваться для того, чтобы уменьшить в целом время загрузки ActiveX через Интернет, например, в случае предстоящего обновления версии ActiveX или при поставке нескольких различных элементов управления ActiveX — в этом случае некоторое число «пакетов», содержащих общую для всех элемен- тов ActiveX или для всех версий данного элемента ActiveX часть скомпилиро- ванного кода, могут быть установлены один раз, а далее будет осуществляться поставка лишь небольшой содержательной части элемента ActiveX. Впрочем, не возбраняется создавать элементы управления ActiveX и в виде одного файла. Отметим также, что при установке флажка Include САи File compression можно собрать используемые файлы в один файл с расширением *.cab, фактически представляющий собой архив, что также уменьшает примерно в два раза время загрузки файлов через Интернет. Следует отметить, что в активных формах можно использовать практически все компоненты Delphi, кроме TMainMenu. Возможна также динамическая генера- ция дополнительных форм в элементе ActiveX па этапе выполнения, при этом дополнительные формы уже не будут содержаться в контейнере, а будут пред- ставлять собой обычные формы Windows (и, естественно, могут содержать в том числе и компонент TMainMenu). Отметим также, что, редактируя библиотеку типов, можно к созданным эле- ментам управления ActiveX добавлять свойства и методы, а затем описывать их реализацию в соответствующем модуле (подобные примеры будут далее рассмот- рены в этой 1лаве). Создание меню с командами открытия диалоговых окон В некоторых случаях использовать страницы свойств элемента ActiveX нельзя или этих страниц недостаточно для реализации всех возможностей по управле- нию элементом. В этих случаях разработчик может создавать и использовать соб- ственные диалоговые окна, заменяющие или дополняющие страницы свойств. При разработке элемента управления ActiveX существует возможность создания дополнительных пунктов меню, которые будут видны па этапе разработки прило- жений, использующих данный элемент управления.
Создание меню с командами открытия диалоговых окон 107 Для иллюстрации этой возможности разработаем элемент ActiveX па основе VCL-комнонента TButton. С этой целью создадим новую библиотеку ActiveX, вы- берем в главном меню команду File ► New ► Other и выберем значок ActiveX control на странице ActiveX репозитария объектов. В качестве VCL-компонента, который следует конвертировать в элемент ActiveX, как уже упоминалось, выберем ком- понент TButton. Далее в модуле реализации выполним следующие изменения. 1. В секции protected перекроем метод PerformVerb: procedure PerformVerb(Verb:integer); override: 2. В секции implementation определим две константы и напишем реализацию ме- тода PerformVerb: const Verbi = 1; Verb2 = 2; procedure TSimpleBX.PerformVerb(Verb: Integer); begin case Verb of Verbi: ShowMessage('First menu item was executed'): Verb2: ShowMessage('And second also'); else inherited PerformVerb(Verb); end; end: 3. Изменим секцию initialization, как показано ниже: initialization with TActiveXControlFactory.Create(ComServer, TSimpleBX. TButton. Class_SimpleBX, 1, ”. 0. tmApartment) do begin AddVerb(Verbi. 'Copyright (C)'): AddVerb(Verb2. '2001 by S.Trepalin'): end; После регистрации и установки в палитру компонентов созданного элемента ActiveX можно поместить его па форму. Если на этапе разработки щелкнуть пра- вой кнопкой мыши па этом элементе управления, появится контекстное меню (рис. 2.16). Рис. 2.16. Дополнительные команды в меню элемента ActiveX
108 Глава 2. Создание элементов управления ActiveX При выборе в меню соответствующих команд будут возникать сообщения, которые были определены в реализации метода PerformVerb. При создании активной формы можно таким же образом добавить новые пункты в меню. Однако в базовом классе активной формы метод PerformVerb (или другой аналогичный метод, который можно было бы перекрыть) не опреде- лен. Поэтому создать какие-либо обработчики событий, связанных с выбором пользователем пунктов такого меню, не представляется возможным. В документации Delphi сказано, что идентификаторы пунктов меню (констан- ты Verbi и Verb2 в проекте) должны быть небольшими целыми числами. В дейст- вительности, если этим идентификаторам присвоить значения, равные, напри- мер, 101 и 102, то никаких новых пунктов в меню не появится. Идентификатор 0 зарезервирован для страниц свойств, поэтому дополнительные идентификаторы меню должны быть равны 1 или больше. Значение 0 следует использовать, если по каким-либо причинам желательно скрыть команду Properties, инициирующую появление страниц свойств. Также следует вызывать метод PerformVerb класса- предка при помощи директивы inherited — иначе страницы свойств не будут отображаться. Получение информации о контейнере При установке элемента ActiveX на форму можно с помощью инспектора объ- ектов изменять его свойства. Новые значения свойств можно анализировать во время разработки или выполнения приложения, если использовать интерфейс lAmbientDispatch. Для иллюстрации этой возможности в предыдущем проекте сде- лаем описанные ниже изменения. 1. В секцию implementation добавим строку с новой константой: Verb3 = 3: 2. В секцию initialization добавим строку: AddVerb(Verb3.'Conta i ner info'): 3. Изменим метод PerformVerb: procedure TSimpleBX.PerformVerb(Verb:i nteger); var Site: ICIleClientSite: Ambients: IDispatch; S: String; begin case Verb of Verbi: ShowMessage('First menu item was executed'): Verb2; ShowMessage('And second also'); Verb3: begin GetCli entSi te(Si te); if Site <> nil then Site.QuerylnterfacedDispatch, Ambients);
Получение информации о контейнере 109 if Ambients <> nil then begin S := 'Display name = ' + lAmbientDispatch(Ambients).DisplayName + #13+#10+'Local ID = ' + IntToStrdAmbientDispatch(Ambients) .LocalelD) + #13+#10+'Back color = ' + IntToStrdAmbientDispatch(Ambients) .BackColor) + #13+#10+'Fore color = ' + IntToStrdAmbientDispatch(Ambients) .ForeColor) + #13+#10; ShowMessage(S): end; end else inherited PerformVerb(Verb); end; end; После добавления этого кода необходимо вновь зарегистрировать сервер в сис- темном реестре (это рекомендуется делать при любом редактировании исходного кода элемента ActiveX). Затем можно поместить полученный элемент ActiveX па форму и свойство Name кнопки изменить на Simpl. При щелчке па элементе правой кнопкой мыши и выборе в меню команды Container Info появится диало- говое окно, показанное слева па рис. 2.17. Рис. 2.17. Диалоговое окно Container Info с начальными значениями (слева) и после изменения цвета формы и имени компонента (справа) Полученное диалоговое окно содержит информацию о контейнере, в частно- сти имя компонента, идентификатор компонента па форме, значения цвета фор- мы и цвета контейнера. Можно изменить цвет формы па зеленый и свойство Name на Button, и тогда при выборе в контекстном меню команды Container Info появит- ся другое диалоговое окно (рис. 2.17, справа). Отметим, что, используя интерфейс lAmbientDispatch, можно лишь опросить контейнер о значениях свойств, по изменить свойства самого контейнера нельзя. При помощи интерфейса lAmbientDispatch помимо вышеперечисленной инфор- мации можно получить сведения о шрифте, единицах измерения, расположении текста па элементе управления ActiveX и о других свойствах
110 Глава 2. Создание элементов управления ActiveX Изменение свойств элемента ActiveX в инспекторе объектов Инспектор объектов (или его аналог в других средствах разработки) считывает текущие значения свойств элемента ActiveX посредством обращения к интер- фейсу IPerPropertyBrowsing. Класс TActiveXControl, который создается автомати- чески при генерации ActiveX, содержит виртуальные методы GetPropertyString, GetPropertyStrings, GetPropertyValue, вызываемые интерфейсом IPerPropertyBrowsing. Изменяя эти методы, можно изменять в инспекторе объектов (или его аналогах, имеющихся в других средствах разработки) строку, которая показывает теку- щее значение свойства. Как правило, инспектор объектов и его аналоги вполне корректно отображают свойства, представимые в виде строк. Но для свойств, которые неудобно или невозможно представить в виде строк (графические изобра- жения и другие ресурсы, шрифт, цвет), программисту необходимо реализовывать интерфейс IPerPropertyBrowsing. В качестве примера можно вернуться к проекту SimpleAXButton и попытаться модифицировать методы интерфейса IPerPropertyBrowsing. Перепишем метод GetPropertyString так, чтобы указатель мыши отображался в квадратных скобках. Для этого в секции protected заголовка класса объявим метод, который будет за- писан вместо уже имеющегося: function GetPropertyString(DispID: Integer; var S: String): Boolean: override; В секции реализации опишем объявленный метод: function TSimpleBX.GetPropertyStringCDispIO: Integer; var S: String): Boolean: begi n if Dispid = 17 then begin S := + IntToStr(Get_Cursor) + Result : = True; end else Result ;= inherited GetPropertyString(DispID, S); end: В качестве параметра метод GetPropertyString получает идентификатор DispID свойства, который для каждого свойства можно найти в реализации библиотеки типов (файл SimpleAXButton_TLB.pas). Для свойства Cursor в пашем случае он ра- вен 17. После компиляции проекта, регистрации элемента ActiveX и установки его в палитру компонентов попробуем поместить этот элемент па форму. Можно за- метить, что указатель мыши появляется в квадратных скобках. Следует обратить внимание, что после указанных модификаций из раскрывающегося списка свой- ства Cursor исчезают все значения. Для того чтобы заполнить раскрывающийся список значениями свойства Cursor, помимо реализации метода GetPropertyString необходимо реализовать два
Навигация по web-страницам 111 других метода — GetPropertyStrings и GetPropertyValue. Они работают в тесной связи друг с другом: первый метод создает строки, которые должны появиться в раскрывающемся списке, а второй формирует соответствующие им значения. Перепишем эти методы для свойства Cursor. В секции protected определения класса поместим определения методов: function GetPropertyStringsCDispID: Integer: Strings: TStrings): Boolean; override: procedure GetPropertyValue(DispID, Cookie: Integer; var Value: 01eVariant): override: В секции реализации напишем код методов: function TSimpleBX.GetPropertyStrings!DispID: Integer: Strings: TStrings): Boolean; begin if Dispid = 17 then begin Strings.Add('First'): Strings.Add('Second'): Strings.Add('Third'): Result ;= True; end else Result := inherited GetPropertyStringsCDispid, Strings); end; procedure TSimpleBX.GetPropertyValue(DispID. Cookie: Integer: var Value: OleVariant): begin if Dispid = 17 then Value := Cookie el se inherited GetPropertyValue(DispId, Cookie, Value): end: Соответственно, после перекомпиляции проекта и установки созданного эле- мента ActiveX па форму можно отметить, что в инспекторе объектов раскрываю- щийся список свойства Cursor содержит созданные в методе GetPropertyStrings строки. Навигация по web-страницам При работе с элементами ActiveX в web-браузерах часто возникает задача смех!ы текущей страницы, отображаемой браузером. Например, если элемент ActiveX используется для формирования сложного запроса, а результаты поиска могут быть представлены в виде динамически генерируемой web-страпицы, то после окончания поиска следует заменить текущую страницу новой.
112 Глава 2. Создание элементов управления ActiveX Наиболее очевидный способ решения этой проблемы — вызвать следующий метод: CreateOleObject('InternetExplorer.Appli cat!on'): Затем можно вызвать его метод Navigate с указанием нового URL-адреса. Если бы приложение Microsoft Internet Explorer было MDI-приложепием, то па этом можно было бы остановиться. Однако Microsoft Internet Explorer является SDI- приложением, поэтому при вызове метода Navigate открывается вторая копия Micro- soft Internet Explorer с повой страницей. Такое поведение приложения не вполне корректно, поскольку пользователь не требовал запустить его вторую копию. Создадим проект, корректно решающий эту проблему. Предположим, необхо- димо ввести список строк и затем по щелчку па кнопке передать его web-серверу для поиска информации. Web-сервер, в свою очередь, создает новую HTML-стра- ницу, которую необходимо поместить вместо текущей страницы. Для полной реализации данной идеи необходимо будет создать ISAPI- или CGI-приложепие и разместить его па web-сервере, что выходит за рамки обсуждаемого здесь мате- риала. Поэтому в данном случае ограничимся просто заменой HTML-страницы. Для ввода строки потребуется однострочное текстовое поле. Поскольку будет вводиться множество строк, нужен объект, который позволит их просматривать — например, список TListBox. Пользователь должен иметь возможность добавлять и удалять строки в списке, а также их редактировать. Имеет смысл создать кнопки, реализующие эти команды. Для простоты ограничимся одной кнопкой, пере- носящей содержимое однострочного редактируемого текстового поля в список. И наконец, надо дать возможность программисту, который будет использовать наш элемент ActiveX для создания своих приложений, производить фильтрацию вводимых данных. Наиболее просто это достигается с помощью обработчика со- бытия, который будет вызываться при переносе текста из однострочного редак- тируемого поля в список. Параметрами этого обработчика станут переносимая строка (WideString) и переменная типа WordBool. Переменную можно изменить в обработчике и тем самым разрешить или запретить перенос строки в список. Кроме того, программисту, который будет использовать элемент ActiveX, надо предоставить возможность изменения его оформления — подписей, цвета и т. д. Создадим новую библиотеку ActiveX, сохраним ее под именем, например LBFill, и добавим в эту библиотеку активную форму командой File ► New ► Other с последующим выбором значка Active Form на странице ActiveX репозитария объ- ектов. В появляющемся диалоговом окне определим имя класса FilledBox, осталь- ные параметры оставим без изменений. На активной форме разместим две кнопки TButton, поле TEdit и список TListBox (рис. 2.18). В библиотеке типов к интерфейсу IFil ledBox добавим повое свойство BtCaption типа WideString. Для этого выделим этот интерфейс и щелкнем па кнопке New Property. Сразу же после этого изменим название нового свойства па BtCaption, а тип свойства — па BSTR (IDL-апалог типа WideString в Pascal). Кроме того, к диспин- терфейсу IFilledBoxEvents добавим новый метод, щелкнув на кнопке New Method: OnBtClick(const EditText: WideString: var CanAppend: WordBool);
Навигация по web-страницам 113 IRItedBoxEvenU El-Л LBFill ® IFilledBox iM>l 4* Deactivate 4» OnClick : J-. -4» OnCieate : -4» OnDbClick , I" 4» OnDesltoy I - 4u OnDeactivate 4s OnKeyPress ; "4« OnPaint j ' 4» OnBtClick FilledBox ф ф TxActiveFormBorderStyle Рис. 2.18. Активная форма с добавленным свойством BtCaption Обратите внимание, что при создании обработчиков событий в редакторе библиотеки типов необходимо перейти на страницу Parameters и в списке Return Туре выбрать пункт void. На той же странице для данного обработчика необходимо добавить два параметра, щелкнув на кнопке Add. Типы параметров обработчи- ка па языке IDL должны быть BSTR и VARIANT BOOL. Чтобы ввести переменную CanAppend после имени типа VARIANT_BOOL, необходимо вручную вставить знак * (в IDL он обозначает указатель) и изменить тип этого параметра па out. Заголовок (свойство Caption) первой кнопки с помощью инспектора объектов изменим па Change page. Затем в редакторе библиотеки типов щелкнем па кнопке Refresh, при этом в модуле реализации создаются заготовки методов для изменения свойства BtCaption. В заготовках поместим следующий код: function IFilledBox.Get_BtCaption: WideString: begin Result := Button?.Caption; end; procedure IFilledBox.Set_BtCaption(const Value; WideStning); begin Button?.Caption := Value: end; Отметим, что после щелчка на кнопке Refresh пе создается заготовки для кода обработчика события OnBtClick. Причина в том, что код обработчика события не реализуется в элементе ActiveX — вместо этого его реализует клиент. В коде эле- мента ActiveX этот обработчик следует вызвать в подходящем месте. Вызов его должен осуществляться при переносе текста из однострочного редактируемого поля в список — то есть в обработчике события OnClick объекта Button?. В создан- ной заготовке напишем следующий код переноса содержимого компонента TEdit в компонент TLi stBox: procedure TFilIedBox.Button?Click(Sender: TObject); var CanAdd: WordBool;
114 Глава 2. Создание элементов управления ActiveX begin CanAdd := True; if FEvents <> nil then FEvents.OnBtClick(Editl.Text, CanAdd); if CanAdd then ListBoxl.Items.AddCEditl.Text); end: Обратите внимание на то, что перед вызовом обработчика события следует проверить, был ли реализован па клиенте нотификациопный интерфейс, ссылка па который хранится в переменной FEvents. Далее, если ссылка па нотификаци- опиый интерфейс присутствует, то обработчик вызывается без проверки факта реализации кода именно для метода OnBtCl ick. Такая проверка бессмысленна, по- скольку если какое-либо приложение реализует код для каких-либо интерфей- сов, то, согласно спецификации СОМ, должны быть реализованы все методы ин- терфейсов. Для замены текущей страницы воспользуемся модулем URLMon. Добавим ссылку па пего в секцию uses и создадим обработчик события OnClick для второй кнопки: procedure TFiIledBox.ButtonlClick(Sender: TObject): begi n HLinkNavigateString(lUnknown(VCLComObject), 'http://localhost/localstart.asp'): end; Зарегистрируем созданный элемент управления ActiveX и выберем в меню Delphi команду Project ► Web deployment options. Заполним открывшееся диало- говое окно (как это сделать, рассказывалось выше в разделе «Создание активных форм»). Обратите внимание па то, что содержимое полей Target Dir и Target URL должно ссылаться па один и тот же каталог, причем этот каталог для посторон- них пользователей открыт только для чтения. Для локального компьютера с ус- тановленными па нем службами Internet Information Services, конфигурация ко- торых оставлена принятой по умолчанию, в качестве содержимого полей Target Dir и HTML Dir следует указать значение C:\lnetPub\WWWRoot, а в качестве содер- жимого поля Target URL — значение http://localhost/ (или http://127.0.0.1/). Затем выберем в меню Delphi команду Project ► Web deploy для переноса эле- мента управления ActiveX и тестовой страницы, включающей ссылку па пего, па web-сервер. Далее в Microsoft Internet Explorer (с соответствующим образом настроенными параметрами безопасности) откроем страницу с элементом управления ActiveX (рис. 2.19). Можно заполнить список и щелкнуть па кнопке Change page. После этого про- исходит замена текущей страницы повой, указанной в методе HLINKNavigateString. Модуль URLMon содержит и другие полезные методы, которые можно исполь- зовать в элементах управления ActiveX, например, показанные ниже методы осуществляют те же операции, что и кнопки Back и Forward браузера: HlinkGoBack(pUnk: IUnknown) HltnkGoForward(pUnk: IUnknown)
Навигация по web-страницам 115 Рис. 2.19. Активная форма в браузере: исходный вид (слева), переход на другую страницу (справа) Данный пример позволяет проиллюстрировать некоторые присущие среде Delphi ошибки. Выберем команду Component ► Import ActiveX Control и поместим импортированный элемент управления па палитру компонентов. Затем создадим повое приложение и поместим элемент управления ActiveX па форму. После этого в инспекторе объектов можно изменить значение свойства BtCaption и убедиться, что па этапе разработки заголовок кнопки изменил название. Теперь запустим приложение и убедимся, что па кнопке по-прежнему присутствует заголовок Button2! Однако заголовок кнопки изменится, если во время работы приложения выполнить следующий код: «ControlName>.BtCaption := ’ааа’: Здесь «Control Name> — имя элемента ActiveX в приложении. Ошибка заключа- ется в том, что па этапе загрузки ресурсов ActiveX все измененные на этапе раз- работки свойства возвращаются к значениям, принятым по умолчанию, однако могут быть легко изменены во время выполнения приложения По-видимому, эта ошибка стала перманентной — опа была в Delphi 3 и сохраняется вплоть до Delphi 7. Что касается нового обработчика событий OnBtClick, то он вызывается корректно, позволяя выполнять фильтрацию текста, вводимого в однострочное редактируемое текстовое поле. И, наконец, возможно, вы станете свидетелем еще одной ошибки (опа про- является не всегда, чаще всего в ранних версиях Delphi). При запуске прило- жения иногда появляется сообщение о том, что не найдено, например, свойство TabOrder. Ошибку эту можно устранить, добавив в редакторе библиотеки типов к интерфейсу IDispatch свойство TabOrder - integer и использовав свойство TabOrder формы в его реализации: function TFilledBox.Get_TabOrder: Integer:
116 Глава 2. Создание элементов управления ActiveX begi n Result := TabOrder; end: procedure IFilledBox.Set_TabOrder(Value: Integer); begin TabOrder : = Value; end: Изменение свойств элемента управления ActiveX на web-странице В предыдущем примере была рассмотрена навигация по web-страшщам при по- мощи команд, генерируемых элементом управления ActiveX. При этом URL-ад- рес был определен в исходном тексте приложения и, соответственно, содержался в самом файле с расширением *.осх. Очевидно, что такой элемент управления ActiveX может обратиться только к одной странице. Если подобный элемент управления предполагается распространять различным пользователям для при- менения на web-страницах, то потребуется перекомпилировать исходный код для каждого пользователя в отдельности, поскольку каждая организация имеет соб- ственный URL-адрес. Более того, если в какой-то организации захотят изменить URL страницы, па которую ссылается элемент управления ActiveX, то снова по- требуется перекомпиляция кода. Если бы браузер Microsoft Internet Explorer имел аналог инспектора объек- тов, можно было бы экспонировать свойство URL, после чего пользователь мог бы определить ссылку самостоятельно. Но такое решение неоправданно, так как возможность выполнения браузером подобных дополнительных операций суще- ственно повышает требования к квалификации конечного пользователя. Для решения проблемы воспользуйтесь интерфейсом IPersistPropertyBag. Этот интерфейс позволяет записать и прочитать свойства элемента ActiveX в попят- ной человеку форме. В частности, Microsoft Internet Explorer версии 3.01 или выше поддерживает этот интерфейс и может считывать свойства элемента ActiveX прямо с web-страницы. ВНИМАНИЕ ---------------------------------------------------------------- В Microsoft Internet Explorer версии 3.00 поддержка интерфейса IPersistPropertyBag отсутствует, и при загрузке элементов управления ActiveX с таким интерфейсом при- ложение аварийно завершается (хотя обычные элементы управления ActiveX прило- жением воспринимаются нормально). За основу возьмем предыдущий проект, хотя все сказанное ниже применимо к любому элементу управления ActiveX. В библиотеке типов определим повое свойство URL типа WideString. В секции private класса TFillBox определим строко- вую переменную FURLAddress:String. После щелчка па кнопке Refresh в секции реализации надо дописать обработчики событий Get_URL и Set URL:
Изменение свойств элемента управления ActiveX на web-странице 117 function TFilledBox.GetJJRL: WideString: begin Result := FURLAddress; end: procedure TFilledBox.Set_URL(const Value: WideString); begin FURLAddress := Value: end. Соответственно, откорректируем уже имеющийся метод ButtonlCl ick в секции реализации: procedure TFilledBox.ButtonlClick(Sender: TObject): var WURL: WideString; begi n WURL := FURLAddress: HLinkNavigateString(IUnknown(VCLComObject). PWideChar(WURD); end: Реализация способа получения данных из HTML-документа зависит от ис- пользуемой версии Delphi. Если элемент управления ActiveX создается в Delphi версии 3 или 4, то интерфейс IPersistPropertyBag необходимо реализовывать вруч- ную. Начиная с Delphi версии 5, интерфейс IPersistPropertyBag реализован в классе TActiveXControl. Реализация метода Load выполнена таким образом, что любое опубликованное свойство может быть прочитано из HTML-документа без каких- либо изменений в коде элемента ActiveX. В созданный Delphi документ LBFill.htm внесем следующие изменения (они отмечены полужирным шрифтом): <HTML> <Н1> Delphi 7 ActiveX Test Page </Н1><р> You should see your Delphi 7 forms or controls embedded in the form below. <HR><center><P> <OBJECT cl assid="clsid:1347FA21-6988-428C-9EC1-E09044971071" codebase=”http://192.168.0.2/LBF ill.cab#vers i on=l.0.0,0" wi dth=245 height=194 align=center hspace=0 vspace=0 <PARAM NAME="COLOR” VALUE="16744576"> <PARAM NAME=''BTCAPTION" VALUE=”HTML read"> <PARAM NAME="URL" VALUE="http://localhost/test.htm"> </OBJECT> </HTML>
118 Глава 2. Создание элементов управления ActiveX Документ LBFill.htm создается после выбора в меню Delphi команды Project ► Web deploy в каталоге, который был указан в качестве значения ноля HTML dir при заполнении диалогового окна Web deployment options. Новые значения свойств должны вводиться после описания объекта, по перед тегом, указывающим на окончание связанных с ним данных (это тег </OBJECT>). После внесения измене- ний можно снова обратиться к странице LBFill.htm (рис. 2.20). Рис. 2.20. Вид элемента управления ActiveX после изменения свойств в HTML-документе Видно, что элемент ActiveX изменил цвет и надпись па кнопке Button2 указан- ными в файле LBFill.htm значениями. Можно щелкнуть па кнопке Change page, и содержимое тестовой страницы Test.htm будет показано в Internet Explorer (не забудьте сначала создать эту страницу!). И, наконец, для тех, кто по-нрежнему работает с Delphi версии 3 или 4, рас- смотрим пример реализации интерфейса IPersistPropertyBag вручную. Код, пред- ставленный ниже, тестировался в Delphi 4. Прежде всего, необходимо сообщить классу TFilledBox, что он должен поддерживать интерфейс IPersistPropertyBag. Для этого следует просто исправить заголовок класса, добавив туда интерфейс IPersistPropertyBag: TFilledBox = class(TActiveForm. IFilledBox, IPersistPropertyBag) Интерфейс IPersistPropertyBag помимо стандартных методов, унаследован- ных от интерфейса IUnknown, имеет четыре дополнительных метода — Load, Save,
Изменение свойств элемента управления ActiveX на web-странице 119 InitNew и GetClassID. Если бы наш элемент ActiveX создавался па основе VCL- компопепта, достаточно было бы реализовать только два метода — Load и Save, поскольку оставшиеся два метода уже реализованы в классе TActiveXControl при реализации поддерживаемого им интерфейса IPersistStreamlnit. Предком класса TActiveForm является класс TCustomForm, в котором отсутствует реализация каких-либо интерфейсов. Поэтому для активной формы придется реализовывать все четыре метода. Для этого в секции private свяжем методы ин- терфейса с методами класса и опишем заголовки методов класса: private { Private declarations } FEvents: IFilledBoxEvents: FURLAddress: String: // IPersistPropertyBag function I PersistPropertyBag.Load = PersistPropertyBagLoad: function IPersistPropertyBag.Save = PersistPropertyBagSave: function IPersistPropertyBag.InitNew = Pers i stPropertyBagIn i tNew; function IPersistPropertyBag.GetClassID = Persi stPropertyBagGetClass ID: function PersistPropertyBagLoad!const pPropBag: IPropertyBag: const pErrorLog : lErrorLog): HRESULT: stdcall: function PersistPropertyBagSavelconst pPropBag: IPropertyBag: fClearDirty: BOOL: fSaveAllProperties: BOOL): HRESULT; stdcall; function PersistPropertyBaglnitNew: HRESULT; stdcall: functi on Pers i StPropertyBagGetClass ID(out ClassID: TCLSID): HRESULT; stdcall; Следует обратить внимание па то, что сначала необходимо связать методы интерфейса и методы объекта, а потом уже описывать их заголовки. Теперь оста- лось реализовать эти же четыре метода в секции реализации: function TFi11edBox.PersistPropertyBagLoad( const pPropBag: IPropertyBag; const pErrorLog: lErrorLog) : HRESULT; var V: Variant; SS: WideString; begi n SS := 'URL': V := Unassigned: pPropBag.Read!'URL', V, pErrorLog); URL := V; pPropBag.Read!'BTCAPTION', V. pErrorLog): BtCaption := V; pPropBag.Read!'COLOR', V, pErrorLog); Color := V;
120 Глава 2. Создание элементов управления ActiveX Result := S_OK: end; function IF111edBox.Pers1stPropertyBagSave( const pPropBag; IPropertyBag; fClearDirty: BOOL; fSaveAllProperties: BOOL): HRESULT; begin Result := EJOTIMPL; end; Если необходимо записать или считать несколько параметров, то функция Read вызывается несколько раз в методе PersistPropertyBagLoad. Также следует обра- тить внимание на реализацию метода PersistPropertyBagSave. В принципе, этот метод не нужен, но поскольку он объявлен в интерфейсе IPersistPropertyBag, то нуждается в реализации, которая типична для методов, которые не поддержи- ваются в текущей реализации интерфейса. Заметим также, что начальная ини- циализация переменной V обязательна, если в качестве браузера используется Microsoft Internet Explorer 3.01 — иначе значение первого свойства не будет прочитано. В более поздних версиях Internet Explorer эта ошибка исправлена. При реализации следующих двух методов необходимо принять во внима- ние, что хотя класс TActiveForm не поддерживает интерфейсы, он регистрируется в фабрике классов как класс TActiveFormControl. Предком класса TActiveFormControl является класс TActiveXControl, поддерживающий множество интерфейсов, вклю- чая интерфейс IPersistStreamlnit, в котором реализованы оставшиеся методы. После успешного запуска фабрики классов будут созданы все интерфейсы, включая IPersistStreamlnit, а проблема останется в обращении к интерфейсу IPersistStreamlnit за реализацией метода. Тут самое время вспомнить о свой- стве VCLComObject, в котором, если оно создано, хранится ссылка на интерфейс IUnknown. А далее, используя метод Querylnteface, можно запросить интерфейс IPersistStreamlnit и вызвать его методы: function TFilledBox.PersistPropertyBaglnitNew: HRESULT: var IP: IPersistStreamlnit: begin Result := E_FAIL; if Assigned(VCLComObject) then if lUnknown(VCLComObject).QueryInterface! IPersistStreamlnit. IP) = S_OK then Result := IP.InitNew; end: functi on TFi11edBox.Persi stPropertyBagGetClass ID( out ClassID: TCLSID): HRESULT: var IP: IPersistStreamlnit; begi n Result := E_FAIL: if Assigned(VCLComObject) then
Создание обработчиков событий в HTML-документах 121 if Hlnknown(VCLComObject).Querylnterface( IPersistStreamlnit. IP) = S_OK then Result := IP.GetClassID(ClassID); end: После всех этих изменений следует вновь зарегистрировать элемент управления ActiveX и внести в HTML-документ содержащий ссылку па пего, список парамет- ров, как это было показано выше. После обращения к странице из Microsoft Internet Explorer можно убедиться в факте чтения параметров из HTML-докумепта. Если элемент управления ActiveX создавать в Delphi 3, то при считывании данных с web-страпицы интерфейсом IPersistPropertyBag могут появиться сооб- щения браузера о потенциально опасном содержимом программы. Как этого из- бежать, будет рассказано чуть позже. Создание обработчиков событий в HTML-документах В HTML-документах можно не только помещать параметры для элементов управ- ления ActiveX, по и создавать обработчики событий па языках VBScript и Java- Script. Сразу же оговоримся, что в спецификацию Sun JavaScript обработчики со- бытий для элементов управления ActiveX пе входят — это разработка Microsoft. Но поскольку возможность использования элементов ActiveX па web-страницах также предоставлена Microsoft, проблема с обработчиками событий решается очень просто: в браузере, где клиент может работать с элементами управления ActiveX, будут работать и обработчики событий. Для создания обработчика событий в HTML-документе воспользуемся пре- дыдущим проектом. Добавим еще одну кнопку, ее свойство Caption изменим на Run Script. В редакторе библиотеки типов отметим диспиптерфейс IFilledBoxEvents и создадим новый обработчик события с именем OnRunScriptCl ick без параметров, как это было описано в разделе «Навигация по web-страницам». В инспекторе объектов создадим обработчик события OnClick для кнопки с заголовком Run Script: procedure TFi11edBox.Button3Click(Sender: TObject); begin if FEvents <> nil then FEvents.OnRunScriptClick; end: Выберем команду Project ► Web deploy. После этого в заново созданный файл LBFill.htm внесем следующие изменения (добавленный код выделен полужирным шрифтом): <HTML> <HEAD> <SCRIPT LANGUAGE=”VBScript"> <! -- Sub TestControlOnRunScriptClick TestControl.BtCaption="Run" End Sub
122 Глава 2. Создание элементов управления ActiveX </SCRIPT> </HEAD> <Н1> Delphi 7 ActiveX Test Page </Hl><p> You should see your Delphi 7 forms or controls embedded in the form below. <HR><center><P> <OBJECT classid="clsid:1347FA21-6988-428C-9ECl-E09044971071" codebase="http://192.168.0.2/LBFi11.cab#version=l.0,0.0" 1d=TestControl width=245 height=194 align=center hspace=0 vspace=0 </OBJECT> </HTML> Для работы с языками сценариев (scripts) элемент управления должен обла- дать идентификатором (строка id =... в теге <OBJECT>) — по имени идентификатора к нему осуществляется доступ из кода па этих языках. Имя обработчика события должно начинаться с идентификатора, затем следует знак подчеркивания (_) и далее — имя обработчика события, как оно определено в диспинтерфейсе эле- мента управления ActiveX. Если обработчик события имеет параметры, то они тоже приводятся. После обращения к странице из Microsoft Internet Explorer и щелчка иа кнопке Run script можно увидеть изменения заголовка кнопки Button2 (рис. 2.21). Рис. 2.21. Результат выполнения кода на языке сценариев в HTML-документе
Система безопасности Internet Exprorer и цифровая подпись 123 Если элемент управления ActiveX создавать в Delphi 3, то при попытке вы- полнения сценария при некоторых настройках Microsoft Internet Explorer могут появиться сообщения браузера о потенциально опасном содержимом программы. Как этого избежать, рассказывается в следующем разделе. Система безопасности Internet Exprorer и цифровая подпись Цифровая подпись помещается в элементы управления ActiveX, которые планиру- ется распространять через Интернет. Для того чтобы получить соответствующий электронный сертификат, необходимо обратиться к уполномоченным компаниям, выдающим такие сертификаты. В этом случае вы должны быть готовы пред- ставить очень подробную информацию о себе (если сертификат частный) или о своей компании. Одна из таких компаний — VeriSign (http://www.verisign.com). После предоставления информации о деятельности компании (или частного лица) VeriSign может выслать соответствующие файлы, если решит, что ваша деятель- ность в области программирования безопасна. Эти файлы требуются для создания электронной подписи под элементом управления ActiveX. В России в настоящее время нет уполномоченных компаний, которые могли бы выдать электронный сертификат международного образца. Электронная подпись, помимо информации о фирме-производителе, песет и ряд другой полезной информации. Так, например, если файл с расширением *.осх был изменен после добавления электронной подписи, то перед запуском та- кого элемента управления об этом будет обязательно сообщено. Для коммерческой разработки элементов управления ActiveX желательно при- обрести пакет Microsoft ActiveX SDK. Помимо детальной документации и ряда полезных ресурсов он содержит программу MAKECER, генерирующую тесто- вые сертификаты. Таким образом, в данный момент получение международного электронного сертификата в пашей стране представляет серьезную проблему. С другой стороны отсутствие электронной подписи приводит либо к постоянным напоминаниям об этом пользователю (или даже запрету па загрузку элемента управления ActiveX при высоком уровне безопасности браузера), либо заставляет его отключить систему безопасности Интернета. Конечно, электронный сертификат не гарантирует отсутствия потенциально опасного содержимого, по, по крайней мере, он позволяет клиенту установить его источник. Кроме того, он перекодирует файл с использо- ванием современных шифровальных алгоритмов и подсчитывает контрольные суммы. Если кто-нибудь попытается внести изменения в код элемента управле- ния ActiveX, такая попытка будет немедленно обнаружена и соответствующий элемент управления ActiveX в Microsoft Internet Explorer работать не будет. Поэтому наличие электронной подписи желательно даже в интрасетях, а уж при работе элемента управления ActiveX в Интернете она просто обязательна. Следующий уровень защиты требуется для тех элементов управления ActiveX, которые могут читать данные из HTML-документа и для обработки событий ко- торых используется код на языках сценариев, содержащийся в HTML-документах.
124 Глава 2. Создание элементов управления ActiveX HTML-документ можно легко редактировать, при этом новые параметры в HTML- документе (как и выполняемые сценарии) для элемента ActiveX могут оказаться бессмысленными. Более того, можно легко создать элемент управления ActiveX, в котором, например, чтение буквы «А» в качестве значения какого-либо свойства будет инициировать форматирование жесткого диска. Такой элемент управле- ния ActiveX при инициализации параметров становится опасным, и разработчик обязан проинформировать Microsoft Internet Explorer о недопустимости инициа- лизации данных и/или выполнения сценариев. Наоборот, если элемент управле- ния ActiveX безопасен, об этом тоже надо проинформировать Microsoft Internet Explorer. Как это сделать, рассказывается далее. Вернемся к предыдущему проекту. Все тесты, описанные здесь, выполнены с Microsoft Internet Information Services 5.0 и Microsoft Internet Explorer 5.0. Уровень безопасности Microsoft Internet Explorer следует настроить, щелкнув в диалоговом окне настройки зон безопасности на кнопке Custom (Другой) и уста- новив переключатель Prompt (Предлагать) во всех группах переключателей, относящихся к системе безопасности элементов управления ActiveX. Если наш элемент управления Fill Box зарегистрирован в системном реестре, за- писи о нем нужно ликвидировать, выбрав в меню Delphi команду Run ► Unregister ActiveX server. После этого следует заполнить диалоговое окно Web Deployment Options. Далее в любом текстовом редакторе откроем HTML-страницу, получен- ную в результате выполнения команды Project ► Web deploy, и поместим туда следующую строку: <PARAM NAME="URL" VALUE="http://localhOSt/’> Кроме того, добавим к тексту страницы сценарий с обработчиком события OnRunScriptClick, как это описано в предыдущем разделе. После этого можно на- чать тестирование системы безопасности Microsoft Internet Explorer. После первого обращения к HTML-странице, содержащей элемент управления ActiveX, происходит его загрузка — он копируется в каталог WINNT\Downloaded Program Files. Далее проверяется наличие электронной подписи (которой в нашем случае нет). Если уровень защиты, установленный в Microsoft Internet Explorer, низкий (low), пользователь получит сообщение о том, что может быть запущено на выполнение потенциально опасное содержимое. Если пользователь не возражает против этого, то происходит регистрация полученного файла с расширением *.осх в системном реестре и при помощи интерфейса IPersistPropertyBag считываются его свойства с HTML-страницы (см. выше). При этом вновь появляется диало- говое окно, сообщающее о том, что хотя наш элемент управления безопасен, он может инициировать выполнение сценариев (рис. 2.22). На группу переключателей Initialize and script ActiveX controls not marked as safe (Использование элементов ActiveX, не помеченных как безопасные) в диалоговом окне Security Settings (Правила безопасности) браузера Microsoft Internet Explorer следует обратить особое внимание. Несмотря на установку переключателя Prompt (Предлагать) и инициализацию свойства URL из HTML-документа, предупреж- дение не было получено. Таким образом, Microsoft Internet Explorer считает
Система безопасности Internet Exprorer и цифровая подпись 125 данный элемент ActiveX безопасным с точки зрения инициализации данных и вы- полнения сценариев. Рис. 2.22. Предупреждение о возможности выполнения сценариев Причина заключается в том, что элементы ActiveX, созданные в Delphi вер- сий 3 и выше, поддерживают интерфейс IDbjectSafety. В этом интерфейсе опре- деляются два метода: function GetInterfaceSafetyOptions(const mid : TGUID; out pdwSupportedOptions : DWORD: out pdwEnabledOptions: DWORD) : HRESULT: function SetInterfaceSafetyOptions(const mid : TGUID; const dwOptionSetMask : DWORD; const dwEnabledOptions: DWORD) : HRESULT: В качестве первого параметра (riid) используется ссылка либо на интерфейс ID 1 spatch, что позволяет использовать ActiveX как сервер автоматизации для кли- ента без его аутентификации, либо на интерфейс IPersistPropertyBag, что разрешает инициализацию данных, либо на интерфейс lActiveScnpt, что разрешает запуск сценариев в элементах ActiveX. Соответственно, в методе GetlnterfaceSafetyOptions в переменных pdwSupportedOptions и pdwEnabledOptions последними версиями Delphi автоматически устанавливается комбинация флагов, определенных в модуле ActiveX.pas: S INTERFACESAFE_FOR_UNTRUSTED_CALLER = 1 — разрешает анонимный доступ к ин- терфейсу; Я INTERFACESAFE_FOR_UNTRUSTED_DATA = 2 — разрешает анонимному пользователю посылать данные в интерфейс. Предположим, что создатель элемента управления ActiveX считает, что при выполнении сценариев или при некорректной инициализации данных этот эле- мент управления может нанести ущерб клиенту. В этом случае он обязан пере- крыть (override) реализованные методы интерфейса IDbjectSafety. Если элемент управления ActiveX реализован в классе TActiveXControl, то это не составляет труда, поскольку оба метода интерфейса IDbjectSafety в секции protected объяв- лены виртуальными. Но для класса-потомка TActiveForm это сделать невозможно, так как активная форма не является потомком класса TActiveXControl. Для того чтобы изменить методы интерфейса IDbjectSafety в активной форме, необходимо
126 Глава 2. Создание элементов управления ActiveX вновь реализовать этот интерфейс. При этом методы нового интерфейса затеня- ют (hide) старые и именно они будут вызываться клиентами. Сначала следует добавить интерфейс 10bjectSafety в список поддерживаемых интерфейсов класса TFi 11 edBox: TFilledBox = class(TActiveForm, IFilledBox. IObjectSafety) Далее в секции private определим два метода GetlnterfaceSafetyOptions и SetlnterfaceSafetyOptions с директивой вызова stdcall, а в секции implementation создадим реализацию этих методов: function TFilledBox.GetInterfaceSafetyOptions(const IID: TIID: pdwSupportedOptions, pdwEnabledOptions: PDWORD): HRESULT; var Unk: IUnknown; begin if (pdwSupportedOptions = nil) or (pdwEnabledOptions = nil) then begin Result ;= E_POINTER; Exit: end; pdwSupportedOptions* := 0: pdwEnabledOptions* : = 0: result := S_OK: end; function TFi11edBox.SetlnterfaceSafetyOpt ions( const IID: TIID; dwOpti'onSetMask. dwEnabledOptions: DWORD): HRESULT; begin Result ;= E_NOTIMPL; end; Результат тестирования этого приложения отличается от предыдущего (рис. 2.23). Рис. 2.23. Результат тестирования элемента управления, объявленного опасным для инициализации и выполнения сценариев Мы видим, что Internet Explorer предупреждает пользователя о возможно опас- ном содержимом. При среднем (middle) уровне безопасности данный элемент
Динамическая инициализация элементов управления ActiveX 127 управления запустится автоматически, по инициализация ег< > данных выполнена не будет, так же как пе будут выполнены и сценарии. Тот же эффект достигается и при щелчке на кнопке No в диалоговом окне, которое предоставляет Microsoft Internet Explorer (см. рис. 2.22). Динамическая инициализация элементов управления ActiveX Хорошо известно, что VCL-компоненты можно создавать динамически, во время выполнения приложения. Например, если в обработчике события, связанного со щелчком на кнопке, выполнить следующий код, то при щелчке на кнопке во время выполнения приложения появится однострочное текстовое поле: procedure TForml.ButtonlClick(Sender: TObject); begi n with TEdit.Create(Self) do begin Parent := Self: Left := 10: Top := 10: end; end; Если бы компонент TEdit отсутствовал на палитре компонентов, то данный код также был бы успешно выполнен — при динамическом создании VCL-kom- понептов совсем не обязательно, чтобы они присутствовали на палитре компо- нентов. Обычно в Delphi с элементами управления ActiveX работают следующим об- разом. Сначала выбирается команда Component ► Import ActiveX control, выбран- ный элемент ActiveX помещается сначала па палитру компонентов, затем — па форму и в инспекторе объектов изменяются свойства и создаются обработчи- ки событий. Возникает вопрос, как инициализировать элемент ActiveX во время выполнения приложения? Вернее, можно ли во время выполнения приложения создать рабочий экземпляр элемента ActiveX, если пе регистрировать его па па- литре компонентов? Из сказанного ранее ясно, что помимо инициализации и создания рабочего экземпляра элемента управления ActiveX для работы приложения требуется создать VCL-контейнер, куда будет помещаться элемент управления ActiveX. Роль такого контейнера в Delphi играет класс Т01 eControl, объявленный в модуле OleCtrls.pas. Базовый метод этого класса — InitControlData. В этом методе необ- ходимо определить GUID фабрики классов элемента управления ActiveX, число обработчиков событий и ссылку реализованного в клиенте интерфейса обработ- чиков событий, а также ссылку па лицензионный интерфейс, необходимый для вызовов методов интерфейса ICIassFactory?. Метод InitControlData вызывается автоматически после отработки конструктора Т01 eControl.
128 Глава 2. Создание элементов управления ActiveX Создадим повое приложение и в секции interface объявим новый класс — по- томок класса Telecontrol: type TRTActiveX = class(TOleControl) protected FControlData; TControlData; procedure InitControlData: override; public constructor EmbeddAX(AOwner: TComponent; AParent: TWinControl; AClassID: TGUID; Rect: TRect); end; Реализация методов InitControlData и EmbeddAX должна выглядеть следующим образом: constructor TRTActiveX.EmbeddAX(AOwner: TComponent: AParent: TWinControl: AClassID: TGUID: Rect: TRect): begi n with FControlData do begin ClassID := AClassID: EventCount := 0: EventDispIDs := nil: LicenseKey := nil; Flags := $00000000; end; inherited Create(AOwner); Self.Parent : = AParent; Self.Visible := True; Self.Left := Rect.Left; Self.Top ;= Rect.Top; Self.Width := Rect.Right-Rect.Left; Self.Height := Rect.Bottom-Rect.Top; end; procedure TRTActiveX.InitControlData; begin Control Data := @FControlData: end; В конструкторе заполняется структура TControlData и создается контейнер в заданной области. В методе InitControlData свойству Control Data присваивается адрес заполненной в конструкторе струщрдо, Qf/paTHTC ВПИМЗПИС ИЗ ТО, ЧТО структура TControlData заполняется до вызова конструктора класса-предка. Так делать не рекомендуется, и необходимость этой конструкции в данном примере
Динамическая инициализация элементов управления ActiveX 129 обусловлена тем, что метод InitControlData, в котором используется структура FControlData, вызывается из конструктора класса-предка. Поместим на форму кнопку и создадим простой обработчик события: procedure TForml.ButtonlClick(Sender: TObject): begin TRTActiveX.EmbeddAX(Self. Self. StringToGUID(’{22D6F312-B0F6-11D0-94AB-0080074C7E95}1). RectClO. 10. 300, 300)); end: Теперь можно запустить созданное приложение и во время его выполнения щелкнуть па кнопке. Элемент управления ActiveX появится в указанной области. Замените идентификатор GUID фабрики классов следующим значением: {0002Е510-0000-0000-С000-000000000046} Результат окажется иным (рис. 2.24). Рис. 2.24. Инициализация незарегистрированного элемента ActiveX во время выполнения приложения Как первый, так и второй из тестируемых здесь элементов управления не были зарегистрированы в палитре компонентов Delphi. В принципе, таким же обра- зом можно обратиться к любому из зарегистрированных в системном реестре COM-серверов, имеющих ключ реестра Control в секции с идентификатором GUID фабрики классов. Наличие этой секции гарантирует поддержку СОМ-сер- вером интерфейсов 101 eClientSite, 101 eControlSite, 101 eInplaceSite, которые необ- ходимы для отображения элемента управления ActiveX в клиенте.
130 Глава 2. Создание элементов управления ActiveX Заключение В настоящей главе мы обсудили вопросы создания и использования элементов управления ActiveX. Мы узнали, что: элементы управления ActiveX представляют собой внутрипроцессные СОМ- серверы, выполняющиеся в адресном пространстве использующего их прило- жения, называемого контейнером; элементы управления ActiveX можно применять в любых средствах разра- ботки, поддерживающих использование в приложениях СОМ-объектов; элементы управления ActiveX нередко используются для расширения возмож- ностей web-браузеров; Delphi позволяет создавать элементы управления ActiveX на основе VCL- компонентов. Мы научились создавать элементы управления ActiveX на основе VCL-компо- нентов, а также тестировать их с помощью средств Visual Basic for Applications. Мы обсудили вопросы создания страниц свойств, позволяющих пользователям менять те или иные свойства элементов управления ActiveX и применяющихся в средствах разработки использующих их приложений. Мы познакомились с созданием активных форм и их тестированием в Internet Explorer, а также обсудили вопросы настройки Internet Explorer для отображе- ния элементов управления ActiveX на web-страницах и распространения элемен- тов управления ActiveX через Интернет. Мы обсудили вопросы создания диалоговых окон, используемых вместо стра- ниц свойств или в дополнение к ним, вопросы считывания и изменения свойств элементов управления ActiveX па этапах разработки и выполнения, вопросы нави- гации по web-страпицам при помощи команд, генерируемых элементом управления ActiveX, вопросы изменения свойств элементов ActiveX с помощью интерфейса IPersistPropertyBag, а также вопросы создания обработчиков событий элементов управления ActiveX на языках VBScript и JavaScript, помещаемых в HTML-до- кументы. Мы уделили внимание вопросам использования цифровой подписи и особен- ностям системы безопасности Microsoft Internet Explorer при работе с элементами управления ActiveX, а также обсудили вопросы динамической инициализации элементов управления ActiveX в приложениях.
ГЛАВА 3 Создание внепроцессных серверов автоматизации Подавляющее большинство современных настольных приложений предназначено для выполнения пользователями тех или иных операций — создания документов, проведения расчетов, анализа данных и т. д. В процессе выполнения подобного рода работ запускаются внутренние сервисы этих приложений, предоставляю- щие пользователям этих приложений некоторые дополнительные возможности, например вычисления значений по формулам, автоматической нумерации абза- цев, заголовков или страниц, проверки правописания и многие другие. Нередко подобные приложения обладают встроенными макроязыками, позволяющими создавать код, использующий эти сервисы, например, в случае часто повторяю- щихся последовательностей операций. Иначе говоря, приложения подобного рода обладают программируемостью. Отметим, однако, что управление приложениями с помощью макроязыков имеет свои недостатки, так как не существует спецификаций, которым должны подчиняться макроязыки. Соответственно, в общем случае у каждого программи- руемого приложения имеется собственный макроязык, отличный от макроязы- ков других программируемых приложений (исключением, пожалуй, являются приложения Microsoft Office, где в качестве макроязыка используется Visual Basic for Applications — подмножество Visual Basic). Было бы гораздо удобней, если бы настольные приложения могли предостав- лять свои специализированные сервисы другим приложениям посредством уни- версального механизма, не зависящего от встроенных макроязыков и позволяю- щего, в частности, использовать обычные языки программирования. Именно для этой цели и предназначен механизм, называемый автоматизацией (automation; ранее — OLE Automation). В этом случае приложение, предоставляющее те или иные сервисы и применяющее для этой цели интерфейсы содержащихся внутри его адресного пространства СОМ-объектов, называется сервером автоматизации. Приложение, использующее эти сервисы, называется контроллером автоматиза- ции и может быть написано с помощью подавляющего большинства современных средств разработки. Отметим, что серверами автоматизации являются, в частно- сти, такие популярные приложения, как Microsoft Office (Word, Excel, PowerPoint), Crystal Reports (Crystal Decisions), Microsoft Internet Explorer, Enterprise-серверы Microsoft (SQL Server, CommerceServer, BizTalk Server, SharePoint Portal Server, Content Management Server и др.), клиентская часть Oracle 7 и Oracle 8, AutoCAD (AutoDesk) и даже сама оболочка Windows 95/98/NT/2000/XP.
132 Глава 3. Создание внепроцессных серверов автоматизации Сервер автоматизации может выполняться в адресном пространстве клиента. В этом случае он называется внутрипроцессным сервером и реализуется в виде динамически загружаемой библиотеки (DLL). Особенности создания таких сер- веров будут рассмотрены в главе 7. Помимо адресного пространства клиента сервер автоматизации может выпол- няться в собственном адресном пространстве, отличном от адресного простран- ства контроллера. В этом случае он называется внепроцессным сервером. Именно о серверах этого типа пойдет речь в данной главе. Если же сервер автоматизации выполняется па компьютере, отличном от компьютера, па котором выполняется контроллер, он называется удаленным сер- вером автоматизации. Отметим, что многие впепроцесспые серверы могут быть запущены как удаленные серверы автоматизации, а некоторые внутрипроцессные серверы могут выполняться в адресных пространствах запущенных удаленно процессов, нередко специально предназначенных для этой цели (таких как рабо- чие процессы Internet Information Services или процессы служб компонентов). Если клиент и сервер находятся в разных адресных пространствах (неважно, па одном компьютере или на разных), то для управления сервером клиент дол- жен обращаться к методам объектов, находящихся в другом адресном простран- стве. Для этой цели используется технология LRPC (Local Remote Procedure Calls — локальные вызовы удаленных процедур). Как было сказано ранее, каждый СОМ-сервер (каковым является сервер автоматизации) и каждый класс COM-объектов обладают идентификатором GIUD — уникальным 128-битовым числом. При обращении к классам СОМ-объ- ектов этот идентификатор иногда называют идентификатором класса (CLSID). При создании COM-серверов (в том числе и серверов автоматизации) в Delphi идентификаторы GUID и CLSID генерируются автоматически, хотя при необходи- мости можно сгенерировать их путем вызова стандартной функции CoCreateGUID COM API. Информация обо всех COM-серверах и классах COM-объектов хра- нится в системном реестре, что позволяет клиенту «пе знать», в каком каталоге (или на каком компьютере локальной сети) находится СОМ-сервер, а получать информацию о нем из реестра. В общем случае СОМ-сервер представляет собой приложение, которое СОМ- объект создает и делает доступным для других программ. Сервер автоматизации предоставляет для доступа объект специального типа — так называемый объект диспетчеризации (dispatch object). При этом в адресном пространстве приложе- ния-контроллера, управляющего сервером, присутствует вариантная переменная, содержащая интерфейс IDispatch, предоставляющий доступ к этому объекту па СОМ-сервере. Контроллер может управлять сервером, например, инициируя его выполне- ние, создание с его помощью документов и иных объектов, изменение размеров, положения и параметров видимости окна сервера, копирование объектов сервера в буфер обмена, добавление данных в созданный сервером документ и т. д. На- личие тех или иных возможностей управления сервером зависит от того, какие объекты, свойства и методы сервера предоставлены разработчиками сервера для обращения к ним из внешних приложений.
Подготовка приложения для создания сервера автоматизации 133 Подготовка приложения для создания сервера автоматизации Для разработки сервера автоматизации следует создать обычное приложение и затем добавить к нему описание классов СОМ-объектов, создаваемых этим приложением и предоставляющих доступ к той части его функциональности, ко- торая должна быть доступна будущим контроллерам автоматизации. Разработаем простейший сервер автоматизации. С этой целью создадим обыч- ное приложение, например, текстовый редактор, содержащий панель инструмен- тов с четырьмя кнопками и компонент ТМето (рис. 3.1), а также диалоговые окна открытия и сохранения файла. Рис. 3.1. Главная форма будущего сервера автоматизации Создадим характерные для текстовых редакторов обработчики событий, свя- занные со щелчками на кнопках: unit UServl: interface uses Windows. Messages. SysUtils, Variants. Classes. Graphics, Controls, Forms. Dialogs. Buttons, StdCtrls. ExtCtrls; type TForml = class(TForm) Panel 1: TPanel: Memol: TMemo: SpeedButtonl: TSpeedButton: SpeedButton2: TSpeedButton: SpeedButton3: TSpeedButton: SpeedButton4: TSpeedButton:
134 Глава 3. Создание внепроцессных серверов автоматизации OpenDi а1og1: TOpenDi а1 од: SaveDialogl: TSaveDialog: procedure SpeedButtonlClick(Sender: TObject): procedure SpeedButton2Click(Sender: TObject): procedure SpeedButton3Click(Sender: TObject): procedure SpeedButton4Click(Sender: TObject): private { Private declarations } public { Public declarations } end: var Forml: TForml; implementation {$R *.dfm} procedure TForml.SpeedButtonlCli ck(Sender: TObject): begin Memol.Lines.Cl ear; end: procedure TForml.SpeedButton2Click(Sender: TObject): begin if OpenDialogl.Execute then Memol.Lines.LoadF romF i1e(OpenDi a1ogl.Fi1 eName): end: procedure TForml.SpeedButton3Click(Sender: TObject): begin if SaveDialogl.Execute then Memol.Li nes.SaveToFi1e(SaveDi alogl.Fi 1 eName); end: procedure TForml.SpeedButton4Cli ck(Sender: TObject); begin Close: end: end. Сохраним проект под именем AutoServ. Отметим, что пока созданный нами текстовый редактор представляет собой обычное Windows-приложение и не яв- ляется сервером автоматизации.
Превращение приложения в сервер автоматизации 135 Превращение приложения в сервер автоматизации Для превращения созданного нами выше приложения в сервер автоматизации вы- берем значок Automation Object на странице ActiveX репозитария объектов (рис. 3.2). Рис. 3.2. Выбор значка Automation Object в репозитарии объектов В открывшемся диалоговом окне введем имя, под которым данный класс COM-объектов будет зарегистрирован в реестре (рис. 3.3). Рис. 3.3. Ввод имени класса В этом же диалоговом окне установим флажок Generate Event support code — это требуется для поддержки нотификационных сообщений в сервере автоматиза- ции (о нотификационных сообщениях будет рассказано в разделе «Нотификаци- онные сообщения во внепроцессных серверах» этой главы). В раскрывающемся списке Threading Model выберем пункт Free, означающий выбор модели свобод-
136 Глава 3. Создание внепроцессных серверов автоматизации пых потоков (free-threaded model). Подробно модели потоков будут обсуждаться в главе 6, сейчас же скажем только, что при использовании модели свободных потоков запросы от разных клиентов выполняются в разных потоках, и будет интересно рассмотреть проблемы, связанные с защитой глобальных переменных. В модели свободных потоков глобальными являются все переменные, объявлен- ные за пределами класса TTest. Понятно, что локальные переменные (то есть пе- ременные, объявленные в процедурах и функциях) в защите не нуждаются. Значение, выбранное в раскрывающемся списке Instancing, определяет, каким образом СОМ-сервер будет реагировать па запросы от нескольких клиентов. Пред- положим, что один из клиентов уже работает с данным COM-сервером, и в этот момент поступает запрос от другого клиента к этому же COM-серверу. При вы- боре в раскрывающемся списке пункта Multiple Instance новый экземпляр СОМ- сервера не запускается — обоих клиентов будет обслуживать созданный ранее экземпляр СОМ-сервера. При выборе пункта Single Instance для каждого клиента будет запускаться отдельный экземпляр СОМ-сервера. И наконец, при выборе пункта Internal фабрика классов СОМ-сервера не будет регистрироваться в систем- ном реестре, следовательно, клиент не сможет обратиться к данному СОМ-сер- веру. Это необходимо в том случае, если данный СОМ-сервер не может работать без другого СОМ-сервера. Например, Microsoft Excel является СОМ-сервером, и одним из свойств объекта Excel .Application, зарегистрированного в системном реестре, является коллекция WorkBooks, каждый элемент которой представляет собой СОМ-сервер с незарегистрированной в реестре фабрикой классов. Если бы фабрика классов была зарегистрирована в системном реестре, то клиент мог бы обратиться к пей, минуя приложение Excel. Получилось бы, что из клиент- ского приложения можно было бы создать таблицу Excel, не загрузив самого приложения Excel.exe. Скорее всего, такой вариант работы разработчиками Excel не предусмотрен, и приложение завершилось бы некорректно. После щелчка на кнопке ОК появится заготовка модуля с рядом определен- ных переменных и методов, связанных с поддержкой потификациоппых сообще- ний — если бы не был установлен флажок Generate Event support code, то они бы отсутствовали. Смысл этих переменных и методов будет обсуждаться в конце главы в разделе «Нотификационпые сообщения во внепроцессных серверах». После этого можно работать с редактором библиотеки типов, в котором нам пред- стоит определить свойства и методы созданного класса СОМ-объектов (рис. 3.4). Если бы не был установлен флажок Generate Event support code, в библиотеке типов отсутствовал бы интерфейс ITestEvents. Библиотека типов Что представляет собой библиотека типов и зачем опа нужна? По существу, биб- лиотека типов — это двоичный файл с описанием интерфейсов СОМ-объекта и их методов. Обычно языком таких описаний является специальный язык IDL, а используются они для того, чтобы разработчики знали, как создать код, реали- зующий методы СОМ-объекта (или вообще методы объекта, расположенного за пределами адресного пространства разрабатываемого приложения, так как IDL используется не только в СОМ-техиологии, но и в иных технологиях, реали-
Библиотека типов 137 зующих вызовы удаленных процедур или функций, например, CORBA). Помимо этого, описания методов на языке IDL могут потребоваться для автоматической генерации (с помощью соответствующих утилит) серверного и клиентского кода для объектов, реализующих маршалинг (marshalling), которыми являются так называемые прокси (proxy) и стаб (stub). Рис. 3.4. Редактор библиотеки типов вновь созданного сервера С другой стороны, код для прокси и стаба может генерироваться динамически. В этом случае клиент должен динамически получать информацию о свойствах и методах интерфейсов COM-объекта, и наличие библиотеки типов, содержащей такую информацию, может быть весьма удобным. Отметим, что библиотеку типов па основе описания на языке IDL можно в принципе сгенерировать с помощью специального компилятора MIDL, по в данном случае в этом пет необходимости. Для редактирования библиотеки типов используется разработанный компа- нией Borland редактор (см. рис. 3.4). В этом редакторе необходимо определить свойства и методы интерфейсов, а также константы. Возникает вопрос — зачем потребовался отдельный редактор? Не проще ли описывать свойства и методы в редакторе кода, как это делается при редактировании классов? Ответ заключается в том, что при добавлении или изменении кода в редакторе библиотеки типов новый код попадает в три интерфейса одновременно: в интерфейс ITypeLibrary, в редактируемый интерфейс и в интерфейс IDispatch (при установленном флажке Dual па странице Flags окна редактора библиотеки типов). Итак, приступим к редактированию библиотеки типов. Предположим, что мы хотим автоматизировать загрузку файла в окно редактора, сохранение набранного текста, очистку окна редактирования, определение и изменение ширины и пара-
138 Глава 3. Создание внепроцессных серверов автоматизации метров видимости формы, а также хотим узнать содержимое компонента Memol. Создадим также метод, добавляющий строку к редактируемому тексту. С этой целью опишем для нашего сервера методы FileNew, FileOpen, FileSave, AddLine и их параметры, а также свойства Text, Width и Visible. Отметим, что для описания параметров методов можно использовать синтак- сис и типы данных IDL либо синтаксис и совместимые с СОМ типы данных Delphi. Для выбора синтаксиса следует обратиться к странице Type Library диалого- вого окна Environment Options (открывается командой Tools ► Environment Options) среды разработки Delphi (напомним, что о типах данных языка IDL рассказыва- ется в разделе «Технология OLE Automation» главы 1). Отметим также, что наряду с типами данных IDL можно использовать все типы данных, определенные в самой библиотеке типов, а также в других библиотеках, на которые опа ссылается. Вернемся к описанию параметров методов нашего объекта автоматизации. Метод NewFi 1 е параметров пе имеет. Методы OpenFi 1 е и SaveFi 1 е имеют один стро- ковый параметр типа BSTR(WideString) — имя файла. Метод AddLine также имеет один строковый параметр, задающий добавляемую строку. Свойство Text доступно для чтения и записи и имеет тип BSTR(WideString). Свойство Visible имеет логиче- ский тип VARIANT_BOOL(WordBool) и тоже доступно для чтения и записи. Свойство Width имеет целый тип long (integer), определяет число пикселов и также доступно как для чтения, так и для записи. После добавления всех свойств и методов библиотека типов должна выгля- деть примерно так, как показано па рис. 3.5. Рис. 3.5. Библиотека типов сервера автоматизации после описания свойств и методов объекта
Реализация методов объекта автоматизации 139 Описав в библиотеке типов все параметры методов, можно приступить к их реализации. Об этом пойдет речь в следующем разделе. Реализация методов объекта автоматизации Итак, мы описали свойства и методы нашего объекта и теперь должны присту- пить к их реализации. Для этой цели в окне редактора библиотеки типов следует щелкнуть на кнопке Refresh панели инструментов. В модуль реализации (сгене- рированный ранее мастером, использованным при создании сервера) будут до- бавлены заготовки методов. Перед тем как начать реализацию кода, следует учесть, что доступ к одному экземпляру сервера может осуществляться несколь- кими клиентами одновременно (в раскрывающемся списке Instancing окна мастера объектов автоматизации выбран пункт Multiple Instance) и из разных потоков (в раскрывающемся списке Threading Model выбран пункт Free). При таком доступе необходима синхронизация обращений клиентов к общим переменным (содер- жимому поля Memol, свойствам Width и Visible) и методам, с ними работающим. Поэтому сначала внесем изменения в модуль, в котором реализуется код формы. Для этого в секции private объявим переменную типа TRTLCriti cal Section, а в сек- ции public определим два метода, как показано ниже: private CS:TRTLCriti са1 Section; public procedure LockForm; procedure UnlockForm: end: Переменную CS: TRTLCriti cal Section необходимо инициализировать. Сделаем это в обработчике события формы OnCreate. Инициализация резервирует систем- ные ресурсы — поэтому по окончании работы их надо освободить. Это делается в обработчике события OnDestroy формы. Реализация кода для формы выглядит следующим образом: procedure TForml.LockForm; begin EnterCriticalSection(CS): end; procedure TForml.UnlockForm: begin LeaveCriticalSection(CS): end: procedure TForml.FormCreate(Sender: TObject): begin InitializeCriticalSection(CS); end;
140 Глава 3. Создание внепроцессных серверов автоматизации procedure TForml.FormDestroy(Sender: TObject); begin DeleteCri 11caI Sect1 on(OS): end: Смысл критических секций будет подробно раскрыт в главе 6 — пока же будем просто иметь в виду, что без них при выполнении кода периодически будет по- являться трудно уловимая и не всегда проявляющаяся ошибка. Далее можно перейти к написанию кода в заготовках методов, объявленных в редакторе библиотеки типов (прежде всего сошлемся на модуль, который со- держит описание формы): function TTest.Get_Width: Integer: begin try Forml.LockForm; Result := Forml.Width; finally Forml.UniockForm; end: end; procedure TTest.Set_Width(Value: Integer); begin try Forml.LockForm; Forml.Width ;= Value; finally Forml.UniockForm; end; end; function TTest.Get_Visible: WordBool; begin try Forml.LockForm; Result ;= Forml.Visible; finally Forml.UniockForm; end; end; procedure TTest.AddLine(const Line: WideString); begin try Forml.LockForm; Forml.Memol.Lines.Add(Line);
Реализация методов объекта автоматизации 141 finally Forml.UnlockForm; end; end: procedure TTest. NewFHe: begin try Forml.LockForm; Forml.SpeedButtonlCl i ck(ni 1); finally Forml.UnlockForm; end; end; procedure TTest.OpenFile(const FileName: WideString): begi n try Forml.LockForm; if Length(FileName)>0 then Forml.Memol.Lines.LoadFromFi1e( FileName) el se Forml.SpeedButton2Cli ck(ni1): finally Forml.UnlockForm; end: end: procedure TTest.SaveFile(const FileName: WideString); begin try Forml.LockForm; if Length(FileName) > 0 then Forml.Memol.Li nes.SaveToFi1e( Fi1eName) el se Forml.SpeedButton3Cli ck(ni 1); finally Forml.UnlockForm; end: end: procedure TTest.Set_Visible(Value: WordBool); begin try Forml.LockForm: Forml.Visible := Value: finally
142 Глава 3. Создание внепроцессных серверов автоматизации Forml.UnlockForm; end; end; function TTest.Get_Text: WideString; begin try Forrnl.LockForm; Result := Forml.Memol.Text; finally Forml.UnlockForm; end; end; procedure TTest.Set_Text(const Value: WideString); begin try Forrnl.LockForm; Forml.Memol.Text := Value; finally Forml.UnlockForm: end; end: Код любого метода начинается с метода LockForm и заканчивается методом UnlockForm. При работе с критическими секциями важно, чтобы после каждого вызова процедуры EnterCriticalSection была выполнена обратная процедура — иначе корректная обработка следующего вызова процедуры EnterCriticalSection становится невозможной. По этой причине обращение к методу UnlockForm поме- щено в защищенный блок try...finally...end. Скомпилируем и запустим сервер на выполнение. При этом он зарегистриру- ется в реестре (рис. 3.6). Рис. 3.6. Запись о сервере автоматизации в реестре Windows В действительности в разделах реестра HKEY_LOCAL_MASHINE\SOFTWARE и HKEY_ CLASSES_ROOT содержится несколько записей, связанных с данным сервером и его интерфейсами, в том числе запись с информацией о местоположении сервера.
Тестирование сервера автоматизации 143 Если в дальнейшем отпадет необходимость в использовании созданного сер- вера автоматизации, рекомендуется запустить его с параметром командной строки /unregserver. В этом случае соответствующие записи будут удалены из реестра. Если же возникнет необходимость в переносе сервера автоматизации в другой каталог, можно после этого просто запустить его снова — записи в реестре об- новятся. Итак, мы создали настольное приложение, являющееся сервером автоматиза- ции. Теперь, основываясь на информации о методах класса объекта автоматизации этого сервера, содержащейся в библиотеке типов, можно создавать управляющие этим сервером приложения-контроллеры с помощью довольно широкого спек- тра средств разработки (включая Delphi, C++Builder, Visual Basic, Visual C++, Visual Studio .NET, и др.). Тестирование сервера автоматизации Теперь, основываясь на информации о методах класса объекта автоматизации, содержащейся в библиотеке типов сервера автоматизации, создадим приложе- ние, управляющее этим сервером. Как было сказано ранее, такие приложения на- зываются контроллерами автоматизации. Создание контроллера автоматизации На главной форме будущего приложения-контроллерс разместим компоненты ТМепто, TEdit, TCheckBox, TOpenDialog, TSaveDialog, а также девять кнопок (рис. 3.7). Рис. 3.7. Главная форма контроллера автоматизации Объявим переменную FServ типа Variant в секции private класса TForml. Соз- дадим обработчики событий, связанные со щелчками на кнопках (при этом сле- дует сослаться на модули ComObj и Variants, указав их в секции uses): procedure TForml.ButtonlClick(Sender: TObject): begin if VarType(FServ) = varDispatch then begin
144 Глава 3. Создание внепроцессных серверов автоматизации FServ := Unassigned; Buttonl.Caption := ’Connect’: end else begin FServ := CreateOLEObjecU'AutoServ.Test'): Buttonl.Caption := 'Disconnect'; end; end; procedure TForml.Button8Click(Sender; TObject): begin if VarType(FServ) = varDispatch then Memol.Text : = FServ.Text: end; procedure TForml Button9Click(Sender: TObject); begi n if VarTypeCFServ) = varDispatch then FServ.Text ;= Memol.Text; end; procedure TForml.Button2Click(Sender: TObject): begin if VarType(FServ) = varDispatch then Editl.Text := FServ.Width; end; procedure TForml.Button3Click(Sender: TObject); begin if VarType(FServ) = varDispatch then FServ.Width := StrToInt(Editl.Text); end; procedure TForml.CheckBoxlClick(Sender: TObject); begin if VarType(FServ) = varDispatch then FServ.Visible := CheckBoxl.Checked; end: procedure TForml.Button4Click(Sender: TObject); begin if VarType(FServ) = varDispatch then FServ.OpenFi1e(Edi 11.Text): end; procedure TForml.Button5Click(Sender: TObject);
Тестирование сервера автоматизации 145 begin if VarType(FServ) = varDispatch then FServ.SaveFile(Editl.Text); end; procedure TForml.Button6Click(Sender TObject); begin if VarType(FServ) = varDispatch then FServ.NewFile: end. procedure TForml.Button7Click(Sender: TObject); begin if VarType(FServ) = varDispatch then FServ.AddLine(Editl.Text); end; Теперь настало время пояснить, что именно делает приведенный выше код. Для управления сервером автоматизации мы создали переменную типа Variant и вызвали функцию CreateOl eObject, содержащуюся в модуле ComObj. При выполне- нии функции CreateOl eObject опа, вызвав несколько функций COM API, создаст экземпляр COM-объекта и вернет его интерфейс IDispatch внутри вариантной переменной. Этот объект содержит интерфейс COM-объекта (в данном случае нашего сервера автоматизации), методы которого мы хотим вызывать из прило- жения. Если исследовать реализацию функции CreateOl eObject в исходном тексте модуля ComObj, можно обнаружить, что она, в свою очередь, вызывает функцию CoCreateInstance COM API, назначение которой — создать объект из исполняе- мого файла или DLL, то есть обратиться к фабрике классов. Переменная типа Variant может содержать разнообразные данные (строку, число и др., в том числе и интерфейс СОМ-объекта). Отметим, что Delphi в отличие от C++ позволяет обращаться к методам и свой- ствам объектов внутри вариантных переменных, при этом существование этих методов и свойств в общем случае может быть заранее неизвестно. Поэтому допус- тимый в Delphi код следующего вида вовсе не означает, что компилятор «знает» о существовании свойства Width вариантной переменной FServ: if VarType(FServ) = varDispatch then FServ.Width:=StrToInt(Edi tl.Text); Дело в том, что при создании контроллеров в Delphi компилятор не проверяет, имеется ли в действительности свойство (в данном случае Width) у объекта, хранящегося в переменной типа Variant (в отличие от объектов другого типа, на- пример TForm), а просто воспринимает название свойства или метода как обыч- ную символьную строку, не анализируя ее содержимое. Поэтому ошибка в на- звании свойства или метода компилятором опознана не будет и проявится лишь
146 Глава 3. Создание внепроцессных серверов автоматизации па этапе выполнения. Попробуйте изменить представленный выше код следую- щим образом: if VarType(FServ)=varD1spatch then FServ.WiGth:=StrToInt(Editl.Text); В этом случае компилятор скомпилирует проект с ошибочным словом WiGth! На этапе выполнения приведенного кода меняется свойство Width объекта, со- держащегося в адресном пространстве сервера, а не созданного контроллера. Соединение с сервером в данном проекте выполняется динамически, при щелчке на кнопке Connect. При этом кнопка работает как выключатель — если соедине- ния с сервером еще нет, сервер запускается и с ним устанавливается соедине- ние, а если есть — соединение разрывается, и сервер выгружается из памяти. Наиболее интересен код, необходимый для отсоединения сервера: вариантной переменной просто присваивается неопределенное значение (unassigned). В этом заключается маленькая хитрость компилятора Delphi: при таком присвоении генерируется код, который проверяет, что находится в вариантной переменной, и если там имеется ссылка на интерфейс, то вызывается его метод Rel ease. По- скольку сервер вызывается динамически, то перед вызовом каждого метода следует проверить, имеется ли в данный момент ссылка па сервер. Это достига- ется с помощью следующего оператора: if VarType(FServ) = varDispatch then ... После запуска контроллера при щелчке на кнопке Connect запускается сер- вер. При щелчке па той же самой кнопке с изменившейся на ней надписью Disconnect он выгружается. При щелчках на кнопках New File, Open File и Save File происходит соответственно очистка окна редактирования, загрузка текста в окно редактирования сервера из файла, сохранение текста в файле. Установка (сня- тие) флажка Visible приводит к появлению (скрытию) сервера. Щелчок на кнопке Get Width приводит к тому, что в текстовом поле в верхней части окна кон- троллера отображается значение ширины окна сервера в пикселах. Если ввести в это поле другое число и щелкнуть па кнопке Set Width, ширина окна сервера станет равной введенному числу пикселов. Щелчок на кнопке Add String приво- дит к тому, что в редактируемый текст компонента ТМето добавляется строка, на- ходящаяся в этот момент во все том же текстовом поле. И наконец, при помощи кнопок Get Text и Set Text можно копировать целиком содержимое компонента ТМето (рис. 3.8). Раннее и позднее связывание В рассмотренном выше контроллере использовалось так называемое позднее связы- вание (late binding). В этом случае, как было сказано выше, анализ существования методов и свойств объекта автоматизации не производится до момента обращения к ним на этапе выполнения. Поэтому при создании контроллера, ориентирован- ного на позднее связывание, высока вероятность внесения незамеченных ошибок в названия методов и свойств.
Тестирование сервера автоматизации 147 Рис. 3.8. Совместное функционирование контроллера и сервера автоматизации Контроллер автоматизации может быть создан и иным способом — путем им- порта библиотеки типов. В этом случае в адресном пространстве контроллера создается набор классов для управления сервером, обладающих теми же самыми методами, что и подлежащий автоматизации объект. Это позволяет непосредст- венно обращаться к методам данных классов, а также выявлять па этапе компи- ляции ошибки в названиях методов сервера. Чтобы иметь возможность создать такой контроллер, следует в меню Delphi выбрать команду Project ► Import Type Library. Откроется диалоговое окно импорта библиотеки типов (рис. 3.9). В списке этого окна необходимо выбрать импортируемый сервер. Строка названия сервера соответствует значению в поле Help String окна редактора библиотеки типов для интерфейса AutoServ: ITypeLibrary (см. рис. 3.4). Обяза- тельно требуется снять флажок Generate Component Wrapper (о нем будет сказано ниже). Для подтверждения выбора следует щелкнуть на кнопке Create Unit (по не Install). В результате будет создан файл с расширением *.pas в каталоге Delphi 7\lmports. Этот файл следует включить в проект контроллера, чтобы сде- лать доступными описания классов для управления сервером (Delphi делает это автоматически).
148 Глава 3. Создание внепроцессных серверов автоматизации Рис. 3.9. Импорт библиотеки типов сервера В этом случае код контроллера можно создать двумя способами. Один их этих способов реализует так называемое раннее связывание (early binding) клиента с сервером, то есть создание и использование в клиентском приложении таблицы виртуальных методов, полностью аналогичной таблице виртуальных методов сервера. Для этого в секции uses модуля реализации клиента нужно сослаться на импортированный модуль AutoServ_TLB. В модуле AutoServ_TLB объявляется класс ITest; а переменную FServ этого типа нужно объявить в секции private класса TForml. Пример реализации методов контроллера автоматизации, созданного с при- менением раннего связывания, приведен ниже. procedure TForml.ButtonlClick(Sender: TObject); begin if Assigned(FServ) then begin FServ := nil: Buttonl.Caption : = 'Connect'; end else begin FServ := CoTest.Create: Buttonl.Caption := 'Disconnect': end: end;
Тестирование сервера автоматизации 149 procedure TForml.Button8Click(Sender: TObject); begin if Assigned(FServ) then Memol.Text := FServ.Text; end; procedure TForml.Button9Cli ck(Sender; TObject); begin if Assigned(FServ) then FServ.Text := Memol.Text; end: procedure TForml.Button2Click(Sender: TObject); begin if Assigned(FServ) then Editl.Text := IntToStr(FServ.Width), end; procedure TForml.Button3Click(Sender: TObject): begin if Assigned(FServ) then FServ.Width := StrTolnt(Editl.Text): end; procedure TForml.CheckBoxlClick(Sender: TObject); begin if Assigned(FServ) then FServ.Visible := CheckBoxl.Checked: end: procedure TForml.Button401ick(Sender; TObject); begin if Assigned(FServ) then FServ.OpenFile(Editl.Text); end; procedure TForml.Button5Cli ck(Sender: TObj ect); begin if Assigned(FServ) then FServ.SaveFile(Editl.Text); end; procedure TForml.Button6Click(Sender: TObject); begin if Assigned(FServ) then FServ.NewFile: end: procedure TForml.Button701ick(Sender: TObject):
150 Глава 3. Создание внепроцессных серверов автоматизации begin if Assigned(FServ) then FServ.AddLine(Editl.Text): end: В этом коде имеются следующие отличия от кода, созданного с применением позднего связывания. Прежде всего, для запуска сервера и получения ссылки на интерфейс используется метод CoTest.Create, который можно вызвать, не созда- вая экземпляра класса, поскольку при его описании в секции interface использу- ется служебное слово cl ass. Этот метод определен в модуле AutoServ_TLB, и при реализации других интерфейсов (не с именем Test) имя класса будет другим. Да- лее, вместо вариантной переменной используется переменная типа ITest. Класс ITest также объявлен в модуле AutoServ_TLB. Поскольку в объявлении этого класса присутствует описание методов, их параметров и свойств, то в приложении- контроллере следует вызывать методы переменной этого класса, и, таким обра- зом, при создании кода приложения все ошибки, связанные с написанием имен методов, будут найдены уже на этапе компиляции. Если, как в примере с ранним связыванием, обратиться к несуществующему свойству FServ.WiGth, то компиля- тор Delphi не позволит использовать аналогичную конструкцию, сгенерировав сообщение об отсутствии у данного класса свойства (метода) с таким названием. Интересны также изменения в методе Button2Click. При позднем связывании значение ширины формы просто помещалось в компонент TEdit: Editl.Text := FServ.Width Этот код работал, поскольку для вариантных переменных автоматически вы- зываются методы преобразования данных. В проекте же, основанном на раннем связывании, компилятор определяет различие в типах данных и требует в явном виде вызова метода преобразования типов. Для проверки наличия ссылки па сервер используется функция Assigned, ко- торая проверяет, равна ли переменная, храпящая ссылку на интерфейс, значе- нию nil. Интересен и способ отсоединения от сервера: переменной, в которой хранится ссылка на интерфейс, присваивается значение nil. Это тоже «обман» компилятора — реально при таком присвоении Delphi проверяет наличие ссылки на интерфейс и, если она есть, вызывает метод Release интерфейса. При раннем связывании поиск необходимого метода при попытке обращения к нему контроллера происходит заметно быстрее, чем при позднем связывании. Однако при замене версии сервера новой соответствие между таблицами вирту- альных методов может быть нарушено, и контроллер окажется неработоспособным. В предыдущем же случае, основанном па позднем связывании и использовании вариантных переменных, этого не произойдет, если, конечно, автор сервера не нарушит спецификацию СОМ, которая содержит требование совместимости версий на уровне свойств и методов. Есть еще и третий способ, основанный также на импорте библиотеки типов, но использующий позднее связывание: TForml=class(TForm)
Тестирование сервера автоматизации 151 private FServ : ITestDisp; procedure TForml.ButtonlCl1ck(Sender: TObject); begin if VarType(FServ) = varDispatch then begin FServ := Unassigned; Buttonl.Caption 'Connect'; end else begin FServ := CreateOLEObject('AutoServ.Test') as ITestDisp: Buttonl.Caption := 'Disconnect'; end: end; В этом случае контроллер автоматизации работает медленнее, чем при ран- нем связывании, однако на этапе компиляции производится синтаксическая проверка кода контроллера на предмет существования свойств и методов объекта автоматизации, к которым он обращается, причем для управления сервером используются свойства и методы классов, созданных при импорте библиотеки типов. Код реализации всех остальных методов полностью идентичен примеру с поздним связыванием, описанному выше. В данном коде следует отметить использование оператора as с интерфейсами: ... := <ссылка на интерфейс* as <имя интерфейса> Синтаксис оператора такой же, как и в случае с классами. При использовании же его с интерфейсами Delphi вызывает метод <ссылка на интерфейс*. Query Interface с GUID для интерфейса <имя интерфейса*. Хотелось бы обратить внимание на следующее Хотя мы и смогли протести- ровать свойства и методы сервера автоматизации с помощью созданного контрол- лера, у нас еще пе было возможности произвести отладку той части кода, которая связана с автоматизацией. Естественно, если клиент запускается под управлением среды разработки Delphi, использовать тот же самый экземпляр среды разработки для отладки сервера нельзя. Поэтому следует открыть проект сервера в от- дельном экземпляре среды разработки и запустить его на выполнение. Если по- сле этого запустить приложение-контроллер (неважно, под управлением другого экземпляра среды разработки или просто средствами операционной системы) и щелкнуть на кнопке Connect, контроллер соединится с уже запущенным экземп- ляром сервера. Если в исходном тексте сервера отмечены точки останова, при их достижении выполнение кода сервера прекращается и управление передается среде разработки. Отметим один очевидный факт: СОМ-сервер и COM-клиент могут быть на- писаны с использованием любых средств разработки, поддерживающих СОМ- технологию. Поэтому в принципе пе возбраняется написать сервер автоматиза- ции в Delphi, а контроллер — в C++Builder или Visual Basic (или наоборот).
152 Глава 3. Создание внепроцессных серверов автоматизации Создание коллекций объектов Во многих серверах автоматизации широко используются коллекции объектов. Представляется естественным задействовать их и в собственных серверах авто- матизации, например в приложениях, ориентированных па работу с документами или какими-либо иными однотипными объектами. Если вернуться к простейшему примеру, рассмотренному в разделе «Превра- щение приложения в сервер автоматизации» данной главы, мы можем обнару- жить в нем подходящий объект для создания коллекции — строки, содержа- щиеся в окне текстового редактора. Модифицируем его, добавив свойства Lines (коллекция строк) и LineCount (число строк, доступных только для чтения), а так- же методы для их считывания и метод для модификации строки с указанным номером (рис. 3.10). Рис. 3.10. Добавление коллекции Lines в библиотеку типов сервера автоматизации Для добавления свойства Li neCount, предназначенного только для чтения, сле- дует раскрыть меню кнопки New Property па панели инструментов окна редактора библиотеки типов и выбрать команду Read Only (рис. 3.11). При создании свойст- ва, предназначенного только для чтения (или только для записи), вместо обоих методов (Get и Put) в интерфейсе будет реализован только один из них. После объявления свойства Lines следует изменить тип свойства на BSTR (WideString), перейти на страницу Parameters и при помощи кнопки Add добавить индексную переменную (рис. 3.12).
Создание коллекций объектов 153 Рис. 3.11. Создание свойства LineCount Рис. 3.12. Редактирование параметров метода GetJJnes для считывания свойства Lines
154 Глава 3. Создание внепроцессных серверов автоматизации После щелчка на кнопке Refresh панели инструментов методы можно реали- зовывать. Код реализации новых методов для считывания вновь введенных свойств Lines и LineCount и для модификации свойства Lines будет выглядеть следующим об- разом: function TTest.Get_LineCount: Integer; begin try Forrnl.LockForm; Result := Forml.Memol.Lines.Count; finally Forml UnlockForm: end; end: function TTest.Get_Lines(Index: Integer): WideString: begin try Forrnl.LockForm; Result := Forml.Memol.Lines[Index]; finally Forml.UnlockForm; end; end; procedure TTest.Set_Lines(Index: Integer: const Value: WideString); begin try Forrnl.LockForm: Forml.Memol.Lines[Index] := Value; finally Forml.UnlockForm; end; end; Теперь модифицируем контроллер, добавив несколько дополнительных ин- терфейсных элементов и несколько обработчиков связанных с ними событий (рис. 3.13). Обработчики событий, связанные со щелчками на кнопках Get line count, Get line и Set line, выглядят следующим образом: procedure TForml.ButtonlOClick(Sender: TObject); begin
Создание коллекций объектов 155 if VarType(FServ) = varDispatch then Edit2.Text :* FServ.LineCount: end: procedure TForml.ButtonllC11ck(Sender: TObject): begin if VarType(FServ) = varDispatch then Editl.Text := FServ.Lines[StrToInt(Edit2.Text) - 1]; end: procedure TForml.Buttonl2Click(Sender: TObject): begin if VarTypetFServ) = varDispatch then FServ.Lines[StrToInt(Edit2.Text) - 1] := Editl.Text: end: Рис. 3.13. Модифицированный контроллер для тестирований сервера с коллекцией строк Поскольку элементы массива, реализующего коллекцию, нумеруются не с 1, а с 0, тогда как строки на экране более привычно начинать нумеровать с 1, в при- веденном коде используется следующая конструкция: FServ.Lines[StrToInt(Edit2.Text) - 1]; Модифицированные сервер и контроллер представлены на рис. 3.14. Итак, мы рассмотрели технические аспекты создания серверов автоматиза- ции, в частности экспонирования их свойств, методов и коллекций. Далее мы поговорим о стандартах, которых принято придерживаться при создании таких серверов.
156 Глава 3. Создание внепроцессных серверов автоматизации Рис. 3.14. Модификация члена коллекции с помощью контроллера автоматизации Экспонируемые свойства и методы В принципе, сервер автоматизации может содержать любые свойства и методы. Однако существует спецификация Microsoft на серверы автоматизации, которой следует руководствоваться при их создании. Методы и свойства, которые, согласно этой спецификации, должны быть реализованы в серверах автоматизации, пе- речислены в представленных ниже таблицах (в табл. 3.1 и 3.2 — для сервера авто- матизации приложения, в табл. 3.3 и 3.4 — для сервера автоматизации документа). В случае SDI-приложепий свойства и методы обоих серверов могут совпадать. Однако в MDI-приложениях необходимо создавать отдельный СОМ-сервер для документа. При этом он не должен регистрироваться в системном реестре, так как документ невозможно показать без приложения. То есть в раскрывающемся списке Instancing окна мастера создания сервера документа (см. рис. 3.3) должен быть выбран пункт Internal.
Экспонируемые свойства и методы 157 Таблица 3.1. Свойства объекта автоматизации приложения Имя Чтение, запись Экспонирование Тип данных Описание ActiveDocument Только чтение Не обязательно IDispatch, unassigned Активный документ Application Только чтение Обязательно IDispatch Приложение Caption Чтение и запись Не обязательно WideString Заголовок приложения DefaultFiI ePath Чтение и запись Не обязательно WideString Путь по умолчанию, используется для открытия/создапия файлов без указания пути Documents Только чтение Не обязательно IDispatch, коллекция Коллекция открытых документов Full Name Только чтение Обязательно WideString Путь и имя приложения Height Чтение и запись Нс обязательно Single Высота главной формы (в режиме MM_HIMETRIС) Interactive Чтение и запись Не обязательно WordBool Возможность изменения документов пользователем Left Чтение и запись Не обязательно Single Координата левого верхнего угла главной формы (в режиме ММ_ HIMETRIC, отсчет от левого верхнего угла экрана) Name Только чтение Обязательно WideString Краткое описание приложения в попятной человеку форме Parent Только чтение Обязательно IDispatch То же, что и свойство Application Path Только чтение Не обязательно WideString Путь к приложению StatusBar Чтение и запись Не обязательно WideString Содержимое строки состояния Top Чтение и запись Не обязательно Single Координата левого верхнего угла главной формы (в режиме ММ_ HIMETRIC, от левого верхнего угла экрана) Visible Чтение и запись Обязательно WordBool Видимость приложения Width Чтение и запись Не обязательно Single Ширина главной формы (в режиме MM_HIMETRIC)
158 Глава 3. Создание внепроцессных серверов автоматизации Таблица 3.2. Методы объекта автоматизации приложения Имя Экспонирование Описание Help He обязательно Показывает справку Quit Обязательно Закрытие приложения Repeat Не обязательно Повторяет последнюю команду пользователя Undo Не обязательно Отменяет последнюю команду пользователя Таблица 3.3. Свойства объекта автоматизации документа из коллекции документов Имя Чтение, запись Экспонирование Тип данных Описание Application Только чтение Обязательно IDispatch Объект приложения Author Чтение и запись Не обязательно WideString Имя автора Comments Чтение и запись Не обязательно WideString Комментарии к документу Full Name Только чтение Обязательно WideString Путь и имя файла с документом Keywords Чтение и запись Не обязательно WideString Ключевые слова для темы Name Только чтение Обязательно WideString Имя документа Parent Только чтение Обязательно IDispatch Родитель документа (может быть объектом приложения) Path Только чтение Обязательно WideString Путь к файлу документа Readonly Только чтение Не обязательно WordBool Возможность редактирования Saved Только чтение Обязательно WordBool Если имеет значение Тrue, документ не менялся с момента сохранения Subject Чтение и запись Не обязательно WideString Тема документа Title Чтение и запись Не обязательно WideString Заголовок документа Таблица 3.4. Методы объекта автоматизации документа из коллекции документов Имя Экспонирование Описание Activate Обязательно Активизация документа Close Обязательно Закрытие всех документов
Нотификацг.энные сообщения во внепроцессных серверах 159 Имя Экспонирование Описание NewW।ndow He обязательно Добавление в документ нового окна Print Обязательно Вывод документа на печать Printout He обязательно То же самое, что и Print PrintPreview He обязательно Предварительный просмотр образца печати RevertToSave He обязательно Откат всех изменений до последнего сохраненного в файле состояния Save Обязательно Сохранение документа SaveAs Обязательно Сохранение документа под другим имепем/в другом формате Можно заметить некоторое дублирование, например, наличие одинаковых методов Pri nt и Pri ntOut. Это связано с тем, что в Visual Basic имеется внутрен- ний метод Print и его использование может привести к путанице. В Delphi также есть совпадающие зарезервированные слова, например Application. При этом Delphi 7 старается дать другое название таким зарезерви- рованным словам, а имя свойства оставляет без изменений. В последних версиях частично были исправлены ошибки, характерные для ранних версий Delphi и про- являвшиеся в том, что в секцию реализации перестают заноситься заготовки методов после щелчка па кнопке Refresh в окне редактора библиотеки типов. При объявлении зарезервированного метода Delphi 7 автоматически изменяет его имя и успешно реализует действия, инициируемые щелчком па кнопке Refresh. Однако при добавлении нового метода или свойства и после щелчка на кнопке Refresh происходит ошибка — в секции реализации появляются как имя заре- зервированного метода (например, Invoke), так и модифицированное имя метода (Invoke_). Единственный способ борьбы с этой ошибкой — не использовать заре- зервированные имена. Нотификационные сообщения во внепроцессных серверах При анонсировании протокола автоматизации OLE в 1995 г. нотификационные сообщения (передача уведомлений от сервера к клиенту) не предусматривались. Например, если на сервере автоматизации менялись данные, он не мог сообщить об этом клиенту, хотя сам клиент мог как угодно часто опрашивать сервер, фиксируя факт изменения данных. В результате частый опрос сервера приводил к резкому возрастанию трафика в сети (если сервер сетевой) или к напрасной трате времени и ресурсов приложения-клиента. Многих разработчиков такая ситуация не устраивала, и это привело к разработке ряда оригинальных методик. Для СОМ-объектов (к которым относится и сервер автоматизации) уведом- ление клиента осуществляется посредством интерфейса IConnectionPoint. Компа- ния Borland впервые реализовала его в Delphi 4.
160 Глава 3. Создание внепроцессных серверов автоматизации Если иа странице ActiveX окна репозитария объектов выбрать значок Automation Object, то в окне мастера создания объектов автоматизации (см. рис. 3.3) можно указать, что желательно создать сервер с поддержкой нотификационных сообще- ний. Для этого нужно установить флажок Generate Event support code. После того как с помощью мастера будет создан сервер автоматизации с под- держкой нотификационных сообщений, в библиотеке типов появятся интерфейс IDispatch и диспиптерфейс. К интерфейсу IDispatch следует добавить свойства и методы, которые будут вызываться из клиента автоматизации. А вот к диснип- терфейсу следует добавить события, о которых сервер будет уведомлять клиента. По сравнению с модулем реализации сервера автоматизации без поддержки нотификаций в модуле реализации библиотеки типов требуется сделать следую- щие изменения. Класс TAutoObject должен экспонировать интерфейс IConnecti onPoi ntContai пег. Я Добавляется переменная FEvents — указатель на интерфейс, передающий по- тификациоппые сообщения клиенту. Этот интерфейс не создается в сервере автоматизации: он создается в клиенте автоматизации и ссылка на пего пере- дается серверу. Добавляется объект FConnecti onPoi nt, в котором реализован интерфейс IConnecti onPoi ntContai пег. Он создается при инициализации сервера автома- тизации. Ш Добавляется метод EventSi nkChanged. Этот метод будет вызываться всякий раз, когда от клиента будет передаваться ссылка на интерфейс приема нотифика- циоипых сообщений. В частности, при выгрузке клиента из памяти будет пе- редаваться указатель nil. М При инициализации сервера создается интерфейс IConnectionPoi nt, который используется для связи с клиентом. Теперь следует рассмотреть последовательность вызовов методов, необходи- мых для поддержки нотификаций СОМ. 1. При разработке клиентского приложения в нем необходимо реализовать дис- пинтерфейс, то есть создать класс с реализацией его методов. Идентификатор GUID и список методов этого диспиптерфейса берутся из библиотеки типов сервера. Для разработанного ранее примера (см. рис. 3.4) это интерфейс ITestDisp. В Delphi 4 для этого приходилось писать код вручную. В более позд- них версиях Delphi появилась возможность автоматически создавать интер- фейс в клиентском приложении путем установки флажка Generate Component Wrapper (см. рис. 3.9). 2. При обращении клиента к COM-серверу и получении ссылки на фабрику клас- сов клиент обращается к серверу за интерфейсом IConnectionPointContaiпег. 3. После получения ссылки на интерфейс IConnectionPointContaiпег клиент вы- зывает его метод FindConnectionPoint. В качестве параметра этого метода ис- пользуется GUID реализованного на клиенте диспиптерфейса, в нашем при- мере — ITestDisp.
Нотификационные сообщения во внепроцессных серверах 161 4. Метод IConnectionPointContaner.FindConnectionPoint реализуется па сервере та- ким образом, что он просматривает GUID всех потификационных диспинтер- фейсов, определенных на сервере (а их может быть несколько). Если запра- шиваемый идентификатор GUID будет найден, то клиенту передается ссылка на интерфейс I Connect! onPoi nt, который уже работает с конкретным потифи- кациопиым диспинтерфейсом. 5. При получении ссылки па интерфейс IConnectionPoi nt клиент вызывает его метод Advise. В качестве параметра этого метода используется ссылка па экземпляр интерфейса, реализованного па клиенте. 6. Сервер при вызове метода IConnectionPoint.Advise вызывает EventSinkChanged. В этом методе запоминается ссылка па клиентский интерфейс в переменной FEvents. Возвращаемый параметр метода IConnectionPoint.Advise — идентифи- катор. 7. Клиент получает и запоминает идентификатор с сервера. Теперь клиент готов принимать нотификации. 8. Когда клиенту более не нужны нотификации (например, перед завершением работы), клиент вновь получает ссылку на интерфейс IConnecti onPoi nt, как это было описано в п. п. 2-4, и вызывает уже другой метод IConnecti onPoi nt. UnAdvi se. В качестве параметра этого метода используется идентификатор, получен- ный в п. 7. 9. Сервер вновь вызывает метод EventSinkChanged с параметром nil, который за- поминается в переменной FEvents. С этого момента нотификации клиенту не передаются. В Delphi 4 всю описанную последовательность вызовов методов (за исключе- нием п. 6) программисту необходимо было кодировать вручную. В последующих версиях Delphi все эти методы вызываются автоматически. Реализуем поддержку нотификаций в проекте AutoServ. Прежде всего надо определить, о каких событиях на сервере должен знать клиент? Первое собы- тие — изменение содержимого компонента ТМето на сервере. Если об этом сооб- щить клиенту, то он может запросить у сервера это содержимое и изменить его, осуществив так называемую «горячую связь». Конечно, в реальных проектах мало кого интересует, изменились ли данные в текстовом редакторе, но данный проект является моделью. Таким же образом будет реализовываться код в реаль- ных проектах — там, например, можно уведомлять клиента об изменении дан- ных на сервере, что достаточно часто требуется на практике. И второе разумное событие, о котором следует сообщить клиенту, — это принудительное закрытие сервера после щелчка па кнопке Close. Если закрыть сервер принудительно (при таком закрытии возникнет предупреждение о нежелательности подобной опера- ции), то на клиенте сохранится ссылка на интерфейс сервера. При первой же по- пытке обратиться к методу этого интерфейса генерируется исключение со сле- дующим сообщением: The RPC server is unavailable
162 Глава 3. Создание внепроцессных серверов автоматизации Чтобы не доводить клиентское приложение до исключения, ему полезно со- общить о закрытии сервера. Ожидаемая реакция клиента в этом случае — отсо- единение от сервера. Откроем созданный ранее проект AutoServ, при необходимости выберем в меню команду View ► Type Library для открытия окна редактора библиотеки типов. В окне редактора выделим интерфейс ITestEvents и добавим два метода — OnTextChange и OnClose, оба без параметров. Кроме того, обработчики событий не должны возвращать результат. Поэтому для каждого из методов на странице Parameters измените содержимое раскрывающегося списка Return Туре на void, как показано па рис. 3.15. Рис. 3.15. Добавление обработчиков событий для сервера автоматизации После добавления новых событий к диспиптерфейсу и щелчка па кнопке Refresh панели инструментов не появляется никаких новых «заготовок» кода ни в одном из модулей, входящих в состав проекта сервера. И этого следовало ожидать: мы сами должны вызвать соответствующий метод в подходящем месте в ответ на событие на сервере. Событие OnTextChange необходимо вызывать из об- работчика OnChange класса ТМешо. Событие OnCl ose необходимо вызывать из обра- ботчика события OnClose класса TForml. Для вызова обработчика событий в этих методах должна быть доступна ссылка на нотификационный интерфейс клиента. Это достигается объявлением предназначенного только для чтения свойства Events:ITestEvents в секции public класса TTest. Однако это еще не все. Следует учитывать, что к одному экземпляру сервера может обращаться несколько клиентов одновременно. Им всем необходимо
Нотификационные сообщения во внепроцессных серверах 163 разослать нотификационные сообщения — то есть серверу потребуется список клиентов. Такой список пе формируется автоматически — его придется фор- мировать в коде. Для формирования списка клиентов можно воспользоваться следующей особенностью сервера автоматизации (если при его создании в спи- ске Instancing окна мастера создания объектов автоматизации был выбран пункт Multiple Instance): для каждого клиента формируется отдельный экземпляр класса TTest. Соответственно, переписав конструктор класса TTest, можно запомнить указатель класса в списке. Но просто список (компонент TList) в данном случае пе подходит, поскольку клиенты работают в разных потоках выполнения и при несинхронном доступе разных клиентов к списку могут происходить исклю- чения. Требуется класс — аналог класса TList, имеющий потокозащищеппые секции. Его можно написать самостоятельно, используя в качестве класса-предка TList, по делать этого не надо. В Delphi имеется класс TThreadList, аналогичный классу TList, но потокозащищенный. Именно в экземпляре этого класса и будет храниться список клиентов. Таким образом, к модулю, содержащему описание класса TTest, следует доба- вить следующий код: interface type TTest = cl ass(TAutoObject, IConnectionPointContainer. ITest) public procedure AfterConstruction: override: destructor Destroy: override: property Events: ITestEvents read FEvents; end; var ClientList: TThreadList = nil; implementation procedure TTest.AfterConstruction: var L:TList: begi n inherited: try L := ClientList.LockList: L.Add(Self): finally Cli entLi st.UniockLi st; end: end:
164 Глава 3. Создание внепроцессных серверов автоматизации destructor TTest.Destroy; var L:TList; N:Integer; begin try L := ClientList.LockList; N := L.IndexOf(Self); If N >= 0 then L.Delete(N); finally ClientLi st.UniockLi st; end; inherited; end: initialization ClientList := TThreadList.Create; finalization ClientList.Free: end. Здесь многоточиями обозначен созданный ранее код. В секции initi 1 ization создается экземпляр класса TThreadLi st, где будет храниться список клиентов. Ссылка на экземпляр располагается в глобальной переменной Cli entLi st, кото- рая объявлена в секции interface — чтобы ее можно было использовать в других модулях. В перекрытом методе AfterConstruction запоминается ссылка на создан- ный экземпляр класса TTest в списке ClientList. Класс TThreadList имеет метод Lock. После вызова этого метода мы получаем ссылку па класс TList и одновре- менно запрещаем клиентам других потоков выполнения работать с этим экземп- ляром класса. После окончания работы обязательно необходимо вызвать метод Uni ockLi st — иначе клиенты, обслуживаемые другими потоками, не смогут ра- ботать с переменной TThreadList. Поэтому вызовы методов LockList и Uni ockLi st помещены в защищенный блок try...finally...end. Теперь можно вызвать добавленные методы OnTextChange и OnClose для каждого из клиентов в классе TForml: procedure TForml.MemolChangeCSender: TObject); var L: TList: I: Integer; T: TTest; begi n try L := ClientList.LockList: for I :=0 to L.Count-1 do begin T := TTest(L[IJ); if Assigned(T.Events) then T.Events.OnTextChange: end: finally
Нотификационные сообщения во внепроцессных серверах 165 ClientList.UniockLi st; end: end: procedure TForml.FormClose(Sender: TObject; var Action: TCloseAction); var L: TList; I: Integer: T: TTest; begin try L := ClientList.LockList: for I ;= L.Count-1 downto 0 do begin T := TTest(L[I]); if AssignedIT.Events) then T.Events.OnClose: end; finally Cl 1entLi st.UniockLi st: end: end: Перед вызовом любого из нотификациопных сообщений необходимо прове- рить факт реализации клиентом нотификациошюго интерфейса и передачи ссылки на пего на сервер. Это достигается вызовом функции Assigned. И еще комментарий по поводу следующего цикла в обработчике события TForml.FormClose: for I := L.Count-1 downto 0 do begin Ожидаемая реакция клиента при получении сообщения об этом событии — отсоединение от сервера. При этом в списке L ссылка па клиента будет удалена, а остальные — смещены по индексам. Для корректного выполнения кода в таких условиях необходимо отсчитывать цикл от больших индексов к меньшим. На этом изменения в сервере можно считать законченными. Теперь создадим новый проект для клиентского приложения с именем Cli Note, выбрав в меню Delphi команду File ► New ► Application. Сразу же следует огово- риться, что этот проект обязательно надо сохранять в ином каталоге, нежели ка- талог, в котором хранится код для сервера AutoServ. Причина — смешение версий файла AutoServ_TLB, одна из которых хранится в каталоге с сервером, а другая — в каталоге $DELPHI\lmports. Для корректной работы клиентского приложения не- обходима версия, которая будет храниться в каталоге $DELPHI\lmports. Поместим па форму компоненты ТМето и TButton. Выберем команду Project ► Import Type Library и в списке появившегося диалогового окна (см. рис. 3.9) выбе- рем пункт AutoServ Library. В отличие от описанного выше проекта, установим флажок Generate Component Wrapper и закроем окно щелчком на кнопке Install. После этого Delphi задаст несколько вопросов, связанных с регистрацией нового компонента. Если все будет успешно выполнено, то па странице ActiveX палитры компонентов появится значок класса TTest. Поместим его на форму.
166 Глава 3. Создание внепроцессных серверов автоматизации Компонент TTest — это сгенерированный Delphi контейнер для сервера автома- тизации. В нем во время выполнения доступны все свойства и методы, которые ранее были определены на сервере в интерфейсе ITest. Кроме того, па странице Events окна инспектора объектов можно увидеть названия методов нотификаци- онного интерфейса — OnTextChange и OnClose. Создавая обработчики этих событий, мы реализуем нотификационный интерфейс па клиенте. Как и в предыдущих проектах, вызов сервера будет осуществляться динамически. Реализуем следую- щий код для клиента: type TForml = class(TForm) private IT: ITest; end: implementation procedure TForml.ButtonlClick(Sender: TObject): begin if Assigned(IT) then begin IT := nil; Testi.Disconnect: Buttonl.Caption := 'Connect'; end else begin Testi.Connect; IT := Testi.Defaultinterface as ITest: Buttonl.Caption := 'Disconnect': end; end; procedure TForml.TestlClose(Sender: TObject): begin IT := nil: Testi.Disconnect: Buttonl.Caption := ’Connect'. end; procedure TForml.TestlTextChangeCSender: TObject); begin Memol.Text := Testi.Text: end: В секции private объявляется переменная IT: ITest, в которой будет храниться ссылка па интерфейс СОМ-сервера. На самом деле, ссылка хранится в качестве значения свойства Defaultinterface класса TTest. Однако, по-видимому, вследст- вие ошибки Delphi это свойство пе инициализировано — при отсоединенном сервере там хранится значение, не равное nil. В обработчике ButtonlClick мы под-
Нотификационные сообщения во внепроцессных серверах 167 ключаемся к серверу, если соединения с ним нет, и наоборот, отсоединяемся, если соединение есть. При получении от сервера нотификации OnTextChange у сервера запрашивается новый текст и помещается в компонент ТМето. При получении со- общения OnCl ose выполняется отсоединение от сервера. Для тестирования проекта лучше запустить несколько экземпляров создан- ного приложения, соединиться с COM-сервером и попробовать изменять содер- жимое компонента ТМето на сервере. Клиенты будут воспроизводить все измене- ния (рис. 3.16). Рис. 3.16. Получение клиентами нотификаций от СОМ-сервера Можно закрыть сервер щелчком па кнопке закрытия, и текст на кнопке кли- ента изменится на Connect, свидетельствуя об отсоединении от сервера. Следует иметь в виду, что теоретически несколько клиентских приложений могут подключиться к одному интерфейсу для получения потификационных сооб- щений. Например, это возможно при использовании более «мягкого» метода — GetActiveObject вместо используемого в CoTest.Create метода CreateComObject. В этом случае клиентскому приложению будет возвращена ссылка на уже созданный интерфейс ITest. Для того чтобы можно было вызвать метод Advise интерфейса IConnectionPoi nt, необходимо сделать перечисленные ниже изменения. В реализации метода Initialize класса TTest изменить флаг ckSingle на ckMulti. В секции public класса TTest объявить предназначенное только для чтения свойство ConnectionPoint типа TConnecti onPoi nt.
168 Глава 3. Создание внепроцессных серверов автоматизации Ж Для вызова метода потификационного интерфейса использовать, например, следующую конструкцию: procedure TForml.MemolChangelSender: TObject): var L: TList; I,J: Integer; T: TTest; El: ITestEvents; begin L := ClientList.LockList; try for I := 0 to L.Count-1 do begin T := TTest(L[I]); for J := 0 to T.Connect!onPoint.SinkList.Count - 1 do begin El ;= IUnknown(T.ConnectionPoint.SinkList[J]) as ITestEvents: if Assigned(EI) then El.OnTextChange: end: end: fi nal1 у Cli entLi st.UniockLi st: end: end: Класс TConnectionPoint в свойстве SinkList содержит ссылки па все клиентские приложения, обратившиеся за потификационным интерфейсом. Опять-таки, здесь присутствует некоторый «обман» Delphi. По спецификации СОМ для получения ссылок па все клиентские приложения надо обращаться к методу Next интерфейса lEnumConnections. Delphi делает это самостоятельно, и программист пользуется ре- зультатами вызова метода Next, хранящимися в свойстве SinkList. В последнее время довольно много внимания уделяется многозвенным при- ложениям для работы с базами данных, использующим технологию DataSnap (подобные приложения будут рассмотрены в главах 12 и 13). Центральное звено в этой архитектуре называется сервером приложений (application server), или сер- вером доступа к данным (data access server), и этот сервер поддерживает прото- кол автоматизации для связи с клиентами. При создании в Delphi удаленного модуля данных (объекта автоматизации сервера приложений) в окне мастера, которое при этом появляется, флажок Generate Event support code отсутствует. Причина в том, что для связи с удаленным сервером может использоваться не- сколько различных технологий — DCOM, протоколы TCP/IP и HTTP. Уведомле- ние же от удаленного сервера без написания дополнительного и довольно объем- ного кода можно получить лишь при помощи технологии DCOM. Заключение В этой главе мы изучили вопросы создания серверов (в качестве которых высту- пали внепроцессные серверы) и контроллеров автоматизации. Мы узнали, что:
Заключение lt>y автоматизация представляет собой универсальный механизм, позволяющий предоставлять сервисы одного приложения другим приложениям с помощью универсального механизма, не зависящего от встроенных макроязыков и по- зволяющего использовать обычные языки программирования; g приложение, предоставляющее ту или иную службу, задействует для этой цели интерфейсы содержащихся внутри его адресного пространства СОМ- объектов и называется сервером автоматизации, а приложение, использую- щее службу, называется контроллером автоматизации; К сервер автоматизации предоставляет своим контроллерам для доступа объект специального типа — объект диспетчеризации (dispatch object). При этом в ад- ресном пространстве приложения-контроллера, управляющего сервером, при- сутствует вариантная переменная, содержащая ссылку на интерфейс IDispatch, предоставляющий доступ к этому объекту па СОМ-сервере. Мы обсудили назначение библиотек типов и узнали, что библиотека типов представляет собой двоичный файл с описанием интерфейсов COM-объекта и их методов. Мы научились создавать библиотеки типов в Delphi. Мы познакомились со способами создания контроллеров. Мы узнали, что: Ж если при создании контроллера автоматизации используется позднее связыва- ние (late binding), то анализ существования методов и свойств объекта авто- матизации пе производится до момента обращения к ним на этапе выполне- ния, поэтому при создании контроллера таким способом высока вероятность незамеченных ошибок в названиях методов и свойств; И если при создании контроллера автоматизации используется раннее связывание (early binding), в адресном пространстве контроллера па основании сведений, полученных при обращении к библиотеке типов, для управления сервером создается набор классов, обладающих теми же самыми методами, что и под- лежащий автоматизации объект; это позволяет непосредственно обращаться к методам данных классов, а также выявить на этапе компиляции ошибки в названиях методов сервера. Мы рассмотрели вопросы создания коллекций объектов в серверах автома- тизации. Мы обсудили стандарты на создание серверов автоматизации, а также реали- зацию механизма уведомления (нотификации) клиентов о событиях сервера по- средством интерфейса IConnectionPoi nt. Обсудив создание серверов и контроллеров автоматизации, мы можем перейти к некоторым практическим задачам, в частности к задачам применения в качестве серверов автоматизации некоторых наиболее часто используемых приложений. Выше мы уже выяснили, что серверами автоматизации являются все последние версии серверных продуктов Microsoft, клиентские части некоторых серверных СУБД, многие Windows-приложения, работающие с документами. Однако наибо- лее часто на практике применяется автоматизация приложений Microsoft Office. Именно этому вопросу и будет посвящена следующая глава.
ГЛАВА 4 Создание контроллеров автоматизации приложений Microsoft Office Данная глава является логическим продолжением предыдущей. Она посвящена автоматизации приложений семейства Microsoft Office, а именно Microsoft Word, Microsoft Excel, Microsoft PowerPoint и Microsoft Outlook — именно эти при- ложения наиболее часто используются не только в качестве пользовательских инструментов, но и в качестве серверов автоматизации. Многие разработчики в процессе работы над своими проектами применяют сервисы, предоставляемые Microsoft Office, такие как средства построения сводных таблиц и диаграмм с по- мощью Microsoft Excel, генерации и печати документов с помощью Microsoft Word и т. д. Нередко пользователи, привыкшие применять приложения Microsoft Office в повседневной работе, сами настаивают па наличии в приложениях таких сервисов или просто па сохранении отчетов и других данных в виде докумен- тов Microsoft Office. Отметим, что потенциальные пожелания подобного рода компанией Microsoft учтены достаточно давно — практически все, что может сделать пользователь любого приложения семейства Microsoft Office (включая не только перечислен- ные выше четыре продукта этого семейства, по и Microsoft Project, Microsoft Visio, Microsoft MapPoint) с помощью меню, клавиатуры и панели инструмен- тов, может быть сделано автоматически, то есть средствами контроллеров авто- матизации. Из наиболее широко известных коммерческих приложений, представляющих собой контроллеры автоматизации Microsoft Office, следует отметить средства приема сертификационного экзамена MOUS (Microsoft Office User). Эти средства представляют собой контроллеры автоматизации соответствующих приложе- ний, анализирующие состояние объектов сервера и на основании этого анализа определяющие, справился ли пользователь с очередным заданием. Это весьма редкий пример вовлечения конечного пользователя в работу сервера — большая часть контроллеров Microsoft Office обычно просто инициирует генерацию доку- ментов па основании данных, которыми манипулирует подобное приложение. Из наиболее известных коммерческих продуктов, использующих подобный способ автоматизации приложений Microsoft Office, можно отметить AllFusion Modeling
Объектные модели Microsoft Office 171 Suite (Computer Associates), а также некоторые средства генерации проектной документации и генераторы отчетов. Сами приложения Microsoft Office также обладают подобной функциональностью (например, именно так работают сред- ства экспорта в Word презентаций PowerPoint). В комплект поставки любого коммерческого сервера автоматизации обычно входит документация и справочные файлы, описывающие их объектную модель. В случае Microsoft Office 2000 — это справочные файлы для программистов на Visual Basic for Applications VBAxxx9.CHM, в случае Microsoft Office XP — файлы VBAxxIO.CHM. Отметим, что по умолчанию они не устанавливаются, так как нужны разработчикам, а не рядовым пользователям. Помимо этого, вся информа- ция об объектах, нужная контроллерам автоматизации, содержится в библиотеках типов (табл. 4.1), имеющих в случае приложений Microsoft Office расширение *.olb (за исключением Excel 2002 — библиотека типов этого приложения на- ходится в самом исполняемом файле Excel.exe). Поэтому при создании прило- жений, использующих раннее связывание, следует генерировать интерфейсный модуль к этим библиотекам. Таблица 4.1. Местоположение справочных файлов по VBA и библиотек типов приложений Microsoft Office Приложение Имя справочного файла Местоположение библиотеки типов Microsoft Word 2000 VBAWRD9.CHM MSWORD9.OLB Microsoft Excel 2000 VBAXL9.CHM EXCEL9.OLB Microsoft PowerPoint 2000 VBAPPT9.CHM MSPPT9.OLB Microsoft Outlook 2000 VBAOUTL9.CHM MSOUTL9.OLB Microsoft Word 2002 VBAWD10.CHM MSWORD.OLB Microsoft Excel 2002 VBAXL10.CHM EXCEL.EXE Microsoft PowerPoint 2002 VBAPP10.CHM MSPPT.OLB Microsoft Outlook 2002 VBAOL10.CHM MSOUTL.OLB Если специально не оговорено, во всех примерах в данной главе используется позднее связывание. Объектные модели Microsoft Office Как уже упоминалось, приложения Microsoft Office предоставляют контролле- рам автоматизации доступ к своей функциональности с помощью своей объект- ной модели, представляющей собой иерархию объектов. Объекты могут предо- ставлять доступ к другим объектам посредством коллекций. В качестве иллюстрации иерархии объектов Microsoft Office приведем неболь- шой фрагмент объектной модели Microsoft Word (рис. 4.1).
172 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office J — коллекции и объекты I ] —объекты Рис. 4.1. Фрагмент объектной модели Microsoft Word В объектных моделях всех приложений Microsoft Office всегда имеется самый главный объект, доступный приложению-контроллеру и представляющий само автоматизируемое приложение. Для всех приложений семейства Microsoft Office он носит название Application, и многие его свойства и методы также одинаковы для всех этих приложений. Ниже представлены те из них, которые мы будем ис- пользовать наиболее часто. В Свойство Visible (доступное для объекта Арр I i cati on всех приложений Micro- soft Office) позволяет приложению появиться па экране и быть представ- ленным в панели задач; оно принимает значения True (пользовательский интерфейс приложения доступен) или False (пользовательский интерфейс приложения недоступен; это значение устанавливается по умолчанию). Если вам нужно сделать что-то с документом Office в фоновом режиме, пе инфор- мируя об этом пользователя, можно пе обращаться к этому свойству — в этом случае приложение можно будет найти только в списке процессов с помощью программы Task Manager. Метод Qui t закрывает приложение Office. В зависимости от того, какое при- ложение Office автоматизируется, он может иметь или не иметь параметры. Общие принципы создания контроллеров автоматизации Microsoft Office В общем случае контроллер автоматизации должен выполнять следующие дей- ствия. 1. Проверить, запущена ли копия приложения-сервера. 2. В зависимости от результатов проверки (либо назначения данного контрол- лера) запустить копию автоматизируемого приложения Office или подклю- читься к уже имеющейся копии. 3. Если необходимо, сделать окно приложения-сервера видимым.
Общие принципы создания контроллеров автоматизации Microsoft Office 173 4. Выполнить какие-то действия с приложением-сервером (например, создать или открыть документы, изменить их данные, сохранить документы и др.). 5. Закрыть приложение-сервер, если его копия была запущена данным контрол- лером, или отключиться от него, если контроллер подключился к уже имею- щейся копии. Соответствующий код для Delphi представлен ниже: uses ComObj. ActiveX procedure TForml.ButtonlClick(Serider: TObject); var ServerlsRunning : Boolean; Unknown : IUnknown; Result : HResult; AppProgID : String; App ; Variant; begin // Указать программный идентификатор приложения-сервера AppProgID := 'Word.Application': ServerlsRunning := False: Result ;= GetActiveObject(ProgIDToClassID(AppProgID).nil.Unknown); if (Result = MK_E_UNAVAILABLE) then // Создать один экземпляр сервера Арр := Created eObject(AppProgID) else begin // Соединиться с уже запущенной копией сервера Арр := GetActiveOleObject(AppProglD); ServerlsRunning := True; end; // показать окно приложения на экране Арр.Visible := True; //------------------------------------------------------ // // Здесь выполняются другие действия И с объектами приложения Office И И------------------------------------------------------- if not ServerlsRunning then App.Quit: App:=Unassigned; end; Здесь мы воспользовались функциями GetActiveOleObject и CreateOl eObject для подключения к уже запущенной копии приложения-сервера или запуска
174 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office новой, если сервер не запущен, что приводит к тому, что в вариантную перемен- ную помещается ссылка па объект Application соответствующего сервера. В заключение сделаем одно маленькое замечание касательно числа парамет- ров методов объектов автоматизации. В случае позднего связывания число ука- занных в коде параметров метода пе обязано совпадать с их истинным числом, которое можно найти в описании объектной модели соответствующего приложе- ния. В этом случае, несмотря на то что метод Quit объекта Application для неко- торых приложений Microsoft Office (например, Microsoft Word) имеет параметры, вполне допустимым является следующий код: Арр.Quit: С другой стороны, при раннем связывании следует строже подходить к опре- делению параметров — их число и типы должны соответствовать описанию ме- тодов в библиотеке типов. Например, в случае раннего связывания корректный код на Delphi для закрытия документа Word со значениями всех параметров по умолчанию будет иметь вид: App.Quit(EmptyParam, EmptyParam, EmptyParam): Помимо перечисленных технических принципов хотелось бы обратить вни- мание на некоторые соображения организационного характера. Как правило, при создании контроллеров автоматизации подобных приложений пе рекомендуется предоставлять конечному пользователю доступ к пользовательскому интерфейсу сервера, по крайней мере, в те моменты, когда свои действия выполняет контрол- лер. В противном случае из-за возможного нежелательного и непредсказуемого вмешательства пользователя результат работы контроллера может оказаться от- личным от ожидаемого. В связи с этим отметим, что манипуляция свойством Visible объекта Application может быть одним из средств решения данной про- блемы — пока это свойство равно False, окна приложения не только невидимы, но и неспособны обрабатывать события, инициируемые мышью или клавиату- рой, и поэтому вмешательство конечного пользователя в их работу исключено. В связи с вышеизложенным отметим, что у объекта Application всех прило- жений семейства Microsoft Office имеется свойство DisplayAlerts, указывающее, выводить или нет па экран диагностические сообщения, которые предназначены пользователю, работающему с приложением интерактивно (например, вопрос о том, перезаписывать ли уже существующий файл). При создании контролле- ров автоматизации приложений Microsoft Office нередко это значение устанав- ливают равным False, поскольку в общем случае, во-первых, контроллер и сер- вер могут быть запущены на разных компьютерах, а во-вторых, как было только что отмечено, сервер может функционировать в режиме, в котором его пользова- тельский интерфейс полностью недоступен, и в этом случае сервером пе обраба- тываются события, инициируемые мышью или клавиатурой (подробнее об этих режимах применения автоматизируемых приложений можно прочесть в главе 11). Обсудив общие принципы создания контроллеров автоматизации и узнав, как это делается средствами Delphi, мы можем перейти к вопросам автоматизации
Автоматизация Microsoft Word 175 конкретных приложений Microsoft Office. Начнем с одного из самых популяр- ных компонентов этого пакета — Microsoft Word. Автоматизация Microsoft Word В данном разделе мы обсудим наиболее часто встречающиеся задачи, связанные с автоматизацией Microsoft Word. Но перед этим рассмотрим программные иден- тификаторы основных объектов Microsoft Word и объектную модель этого при- ложения. Программные идентификаторы и объектная модель Microsoft Word Объекты, непосредственно доступные приложению-контроллеру, представлены в табл. 4.2. Таблица 4.2. Объекты Word, доступные непосредственно приложению-контроллеру Объект Программный идентификатор Комментарий Application Word. Application, Word.Application.9 (10) С помощью этого программного идентификатора создастся экземпляр Word без открытых документов Document Word.Document, Word. Document. 9 (10), Word.Tempi ate.8 С помощью этого программного идентификатора создается экземпляр Word с одним вновь созданным документом Все остальные объекты Word являются так называемыми внутренними (inter- nal) объектами. Это означает, что они не могут быть созданы сами по себе; так, объект Paragraph (абзац) не может быть создан отдельно от содержащего его до- кумента. Отметим, что последнее число в программном идентификаторе объекта со- ответствует номеру версии продукта (9 — Microsoft Office 2000, 10 — Microsoft Office XP). Если вспомнить, что основное назначение приложения Word — работа с до- кументами, можно легко попять иерархию его объектной модели (ее фрагмент был показан па рис. 4.1). Основным объектом в ней, как и в объектных моделях других приложений Microsoft Office, является объект Application, содержащий коллекцию Documents объектов типа Document. Каждый объект типа Document содер- жит коллекцию Paragraphs объектов типа Paragraph, коллекцию Bookmarks объек- тов типа Bookmark, коллекцию Characters объектов типа Character и т. д. Манипу- ляция документами, абзацами, символами, закладками реально осуществляется путем обращения к свойствам и методам этих объектов. Ниже мы рассмотрим наиболее часто встречающиеся задачи, связанные с авто- матизацией Microsoft Word.
176 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office СОВЕТ ---------------------------------------------------------------------- Если вам встретилась задача, пе совпадающая ни с одной из рассмотренных в дан- ной главе, можно попытаться пайти подходящий пример в справочной системе Visual Basic for Applications или просто записать необходимую последовательность действий в виде макроса и проанализировать его код. Создание и открытие документов Microsoft Word При разработке примеров использования Microsoft Word можно модифициро- вать приведенный ранее код создания контроллера, заменив комментарии кодом, манипулирующим свойствами и методами объекта Word.Application. Мы начнем с создания и открытия документов. Создать новый документ Word можно, используя метод Add коллекции Documents объекта Appl i cat ion: App.Documents.Add: Как создать нестандартный документ? Очень просто — нужно указать имя шаблона в качестве параметра метода Add: Арр.Documents.Add( 'C:\Program Fi1es\Microsoft Office\Templates\1033\Manual .dot'); Для открытия уже существующего документа следует воспользоваться мето- дом Open коллекции Documents: Арр.Documents.Open('C:\MyWordFile.doc'): Отметим, что свойство ActiveDocument объекта Word.Application указывает на текущий активный документ среди одного или нескольких открытых. Помимо этого, к документу можно обращаться по его порядковому номеру с помощью метода Item; например, ко второму открытому документу можно обратиться так: Арр.Documents.Item(2) Отметим, что нумерация членов коллекций в Microsoft Office начинается с еди- ницы. Сделать документ активным можно с помощью метода Activate: Арр.Documents.Item(1).Acti vate: Следующее, что следует научиться делать, — это сохранять документ Word и закрывать само приложение Word. Сохранение, печать и закрытие документов Microsoft Word Закрытие документа может быть осуществлено с помощью метода Cl ose одним из следующих способов: Арр.Documents.Item(2).Close: App.Acti veDocument.Cl ose:
Автоматизация Microsoft Word 177 Метод Close имеет несколько необязательных (в случае позднего связывания) параметров, влияющих на правила сохранения документа. Первый из них опре- деляет необходимость сохранения внесенных в документ изменений и принимает три возможных значения (соответствующие константы рекомендуется описать в приложении): const wdDoNotSaveChanges = $00000000: // не сохранять изменения wdSaveChanges = JFFFFFFFF: И сохранять изменения wdPromptToSaveChanges = SFFFFFFFE: // вывести диалоговое окно И с соответствующим запросом Второй параметр определяет формат сохраняемого документа: const wdWordDocument = $00000000: И сохранить в // формате Hord wdOriginalDocumentFormat = $00000001: // сохранить в исходном И формате документа wdPromptUser = $00000002; // вывести диалоговое И окно Save As Третий параметр принимает значения True или False и определяет необходи- мость пересылки документа следующему пользователю по электронной почте. Если такая функциональность пе нужна, этот параметр можно проигнорировать. Таким образом, при использовании перечисленных выше параметров закрыть документ можно, например, так: App.ActiveDocument.Close(wdSaveChanges, wdPromptUser): Просто сохранить документ, не закрывая его, можно с помощью метода Save: Арр.Act iveDocument.Save; Этот метод также имеет несколько необязательных (в случае позднего свя- зывания) параметров, первый из которых равен True, если документ сохраня- ется автоматически, и False, если нужно выводить диалоговое окно для получе- ния подтверждения пользователя о сохранении изменений (если таковые были сделаны). Второй параметр влияет на формат сохраняемого документа, и спи- сок его возможных значений совпадает со списком значений второго параметра метода Cl ose. СОВЕТ --------------------------------------------------------------------- При создании контроллеров автоматизации, использующих позднее связывание, можно тем пе менее сгенерировать интерфейсный модуль для соответствующей библиоте- ки типов и сохранить его в виде текстового файла, пе включая сам модуль в проект. В этом случае можно будет воспользоваться содержащимися в тексте интерфейс- ного модуля определениями значений констант, копируя их в код приложения-кон- троллера.
178 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Напоминаем, что закрыть само приложение Word можно с помощью метода Quit объекта Word.Application. Этот метод имеет в общем случае три параметра, совпадающих с параметрами метода Cl ose объекта Document. Вывод документа на устройство печати можно осуществить с помощью метода Printout объекта Document, например: Арр.Acti veDocument.Pri ntOut: Если нужно изменить параметры печати, следует указать значения соответст- вующих параметров метода Printout (их в случае Microsoft Office около двадцати). Вставка текста и объектов в документ и форматирование текста Для создания абзацев в документе можно использовать коллекцию Paragraphs объекта Document, представляющую собой набор абзацев данного документа. Доба- вить новый абзац можно с помощью метода Add этой коллекции: Арр.ActiveDocument.Paragraphs.Add: Для вставки собственно текста в документ применяется не объект Paragraph, а объект Range, представляющий любую непрерывную часть документа (в том числе и вновь созданный абзац). Этот объект может быть создан разными спосо- бами. Например, можно указать начальный и конечный символы диапазона (если таковые имеются в документе): var Rng : Variant: Rng := App.ActiveDocument.Range(2, 4): // co 2-го по 4-й символы Можно также указать номер абзаца (например, только что созданного): Rng := Арр.ActiveDocument.Paragraphs.Item(l).Range: Кроме того, можно указать несколько последовательных абзацев: Rng := Арр.ActiveDocument.Range (Арр.Acti veDocument.Pa ragraphs.Item(3).Range.Start. App.Acti veDocument.Paragraphs.Item(5).Range.End) Вставить текст можно с помощью методов InsertBefore (перед диапазоном) или InsertAfter (после диапазона) объекта Range, например: Rng.InsertAfter(‘Это вставляемый текст'); Помимо объекта Range текст можно вставлять с помощью объекта Selection, являющегося свойством объекта Word.Application и представляющего собой вы- деленную часть документа (этот объект создается, если пользователь выделяет часть документа мышью, и может быть создан также с помощью приложения-
Автоматизация Microsoft Word 179 контроллера). Сам объект Selection можно создать, применив метод Select к объ- екту Range, например: van Sei : Variant: Арр.Acti veDocument.Paragraphs.Item(3).Range.Seiect: В приведенном выше примере в текущем документе выделяется третий абзац. Если мы хотим вставить строку текста в документ либо вместо выделенного фрагмента текста, либо перед ним, это можно сделать с помощью следующего фрагмента кода: van Sei; Variant: Sei := App.Selection; Sei.TypeTextC'Это текст,' + ' которым мы заменим выделенный фрагмент'): Отметим, что если свойство Options.ReplaceSelection объекта Word.Application равно True, выделенный текст будет заменен новым (этот режим действует по умолчанию); если же нужно, чтобы текст был вставлен перед выделенным фраг- ментом, а не вместо него, следует установить это свойство равным False: Арр.Options.ReplaceSelection : = False; Символ конца абзаца при использовании объекта Selection может быть встав- лен с помощью следующего фрагмента кода: Sei.TypeParagraph; К объекту Selection, так же как и к объекту Range, можно применить методы InsertBefore и InsertAfter. В этом случае, в отличие от предыдущего, вставляе- мый текст станет частью выделенного фрагмента текста. С помощью объекта Selection, используя его свойство Font и свойства объекта Font, такие как Bold, Italic, Size и другие, можно отформатировать текст. Напри- мер, таким образом можно вставить строку, выделенную полужирным шрифтом: Sei.Font.Bold := True; Sei.TypeTextC'Это текст, который выделен полужирным шрифтом.'); Sei.Font.Bold := False: Sei.TypeParagraph: Для наложения па вставляемый текст определенного заранее стиля можно использовать свойство Style объекта Selection, например: Sei .Style := 'Heading 1'; Sei .TypeTextC'Это текст, который станет заголовком'): Sei.TypeParagraph:
180 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Нередко документы Word содержат данные других приложений. Простей- ший способ вставить такие данные в документ — использовать метод Paste объ- екта Range: var Rng: Variant: Rng := Арр.Select!on.Range; Rng.Col 1 apse(wdCol1apseEnd) Rng.Paste: Естественно, в этом случае в буфере обмена уже должны содержаться встав- ляемые данные. Если нужно поместить в буфер обмена часть документа Word, это можно сде- лать с помощью метода Сору объекта Range: var Rng: Variant; Rng := Арр.Select!on.Range; Rng.Copy; Следующее, что нужно научиться делать, — это перемещать курсор в нужное место текста, чем мы и займемся в следующем разделе. Перемещение курсора по тексту Используя метод Collapse, можно «сжать» объект Range или объект Selection, со- кратив его размер до пуля символов: Rng.Collapse(wdCollapseEnd); Параметр этого метода указывает, где окажется новый объект Range или Selection — в начале или в конце исходного фрагмента. Если используется позд- нее связывание, нужно определить в приложении соответствующие константы: const wdCollapseStart = $00000001; // новый объект находится Ив начале фрагмента wdCollapseEnd = $00000000; // новый объект находится Ив конце фрагмента Перемещать курсор по тексту можно с помощью метода Move объектов Range и Selection. Этот метод имеет два параметра. Первый из них указывает па то, в ка- ких единицах измеряется перемещение — в символах (по умолчанию), словах, предложениях, абзацах и др. Второй параметр указывает па число единиц, на ко- торое нужно переместиться (это число может быть и отрицательным; по умолча- нию оно равно 1). Например, следующий фрагмент кода приведет к перемеще- нию курсора на один символ вперед: Rng.Move;
Автоматизация Microsoft Word 181 Другой фрагмент кода приведет к перемещению курсора па три абзаца вперед: Rng.Move(wdParagraph, 3): Отметим, что этот метод использует следующие константы const wdCharacter wdWord wdSentence wdParagraph wdStory // Единицей перемещения является-. = $00000001; // символ = $00000002; // слово = $00000003; // предложение = $00000004; // абзац = $00000006; // часть документа // (например, колонтитул, оглавление и др.) wdSection wdColumn wdRow wdCell wdTable = $00000008; // раздел = $00000009; // колонка таблицы = $0000000А; // строка таблицы = $00000000; // ячейка таблицы = $0000000F; // таблица Нередко для перемещения по тексту используются закладки. Создать за- кладку в текущей позиции курсора можно путем добавления члена коллекции Bookmarks объекта Document с помощью метода Add, указав имя закладки в качестве параметра, например: Арр.Acti veDocument.Bookmarks.Add('MyBookmark'): Проверить существование закладки в документе можно с помощью метода Exists, а переместиться на нее — с помощью метода Goto объекта Document, Range или Selection: Rng := App.ActiveDocument.Goto(wdGoToBookmark. wdGoToNext. .'MyBookmark'); rng.InsertAftert'Текст, вставленный после закладки’); Значения констант для этого примера таковы: const wdGoToBookmark = $FFFFFFFF; // перейти к закладке wdGoToNext = $00000002: // искать следующий объект в тексте Отметим, что с помощью метода Goto можно перемещаться не только на указан- ную закладку, но и па другие объекты (рисунки, грамматические ошибки и др.), и направление перемещения тоже может быть разным. Поэтому список констант, которые могут быть использованы в качестве параметров данного метода, довольно велик. Создание таблиц Создавать таблицы можно двумя способами. Первый заключается в вызове метода Add коллекции Tables объекта Document и последовательном заполнении ячеек дан- ными. Этот способ при позднем связывании работает довольно медленно.
182 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Второй способ, который намного «быстрее», заключается в создании текста из нескольких строк, содержащих подстроки с разделителями (в качестве разде- лителя можно использовать любой или почти любой символ, но нужно, чтобы он заведомо пе встречался в данных, которые в дальнейшем будут помещены в таблицу), и последующего преобразования такого текста в таблицу с помощью метода ConvertToTable объекта Range. Ниже приведен пример создания таблицы из трех строк и трех столбцов этим способом (в качестве разделителя, являющегося первым параметром метода ConvertToTable, используется запятая): var Rng: Variant: Rng := App.Selection.Range: Rng.Coll apse(wdCol1apseEnd); Rng.InsertAfter!'1. 2. 3'): Rng.InsertParagraphAfter; Rng.InsertAfter(’4.5,6'); Rng.InsertParagraphAfter: Rng.InsertAftert'7.8.9'); Rng.InsertParagraphAfter; Rng.ConvertToTable!’,'); Отметим, что внешний вид таблицы можно изменить с помощью свойства Format, а также с помощью свойств коллекции Col limns, представляющей колонки таблицы, и коллекции Rows, представляющей строки таблицы (объекта Table). Обращение к свойствам документа Свойства документа можно получить с помощью коллекции BuiltlnDocumentProperties объекта Document, например: Memol.Lines.Add!'Название - ' + App.ActiveDocument. Bui 1tlnDocumentProperti es[wdPropertyTitle].Vaiue); Memol.Lines.Add('Автор - ' + App.ActiveDocument. BulltlnDocumentProperties[wdPropertyAuthor].Value); Memol.Lines.Add('Шаблон - ' + App.ActiveDocument. BuiltInDocumentProperties[wdPropertyTemplate],Value): Константы, необходимые для обращения к свойствам документа по имени, приведены ниже: const wdPropertyTitle = $00000001 // название wdPropertySubject = $00000002 II назначение wdPropertyAuthor = $00000003 И автор wdPropertyKeywords = $00000004 И ключевые слова wdPropertyComments = $00000005 II комментарии wdPropertyTempl ate = $00000006 И шаблон wdPropertyLastAuthor = $00000007 И автор, последним II редактировавший текст
Автоматизация Microsoft Excel 183 wdPropertyRevision = $00000008; // версия wdPropertyAppName = $00000009: // имя приложения wdPropertyTimeLastPrinted $0000000A; // // дата последнего вывода на устройство печати wdPropertyT i recreated = $00000008; // время создания wdPropertyTimeLastSaved = $00000000; // // время последнего сохранения wdPropertyVBATotal Edit = $00000000; // // суммарное время редактирования wdPropertyPages = $00000008; // число страниц wdPropertyWords = $00000008: // число слов wdPropertyCharacters = $00000010: // число символов wdPropertySecuri ty = $00000011: // // правила доступа к документу wdPropertyCategory = $00000012; // категория wdPropertyFormat = $00000013; // формат документа wdPropertyManager = $00000014; // менеджер wdPropertyCompany = $00000015: // компания wdPropertyBytes - $00000016; // число байт wdPropertyLi ries = $00000017; // число строк wdPropertyParas = $00000018; // число абзацев wdPropertySlides $00000019; // число слайдов wdPropertyNotes = $0000001A; // число комментариев wdPropertyHiddenSlides = $0000001B: // // число скрытых слайдов wdPropertyMMClips = $00000010; // // число нультимедиа-клипов wdPropertyHyperlinkBase = $00000010; // // путь к гипертекстовым ссылкам wdPropertyCharsWSpaces = $0000001E: II и число символов без учета пробелов Итак, в данном разделе мы изучили основные операции, которые наиболее часто применяются при автоматизации Microsoft Word. Естественно, возможно- сти автоматизации Word далеко пе исчерпываются приведенными примерами, однако мы надеемся, что, пользуясь изложенными здесь основными принципами создания контроллеров Word и соответствующим справочным файлом но VBA, вы сможете создавать контроллеры автоматизации Microsoft Word самостоятель- но — мы с вами уже убедились, что это действительно пе так уж сложно. Следующим приложением Microsoft Office, автоматизацию которого мы рассмот- рим, будет Microsoft Excel — второе по популярности приложение Microsoft Office. Автоматизация Microsoft Excel В данном разделе мы обсудим наиболее часто встречающиеся задачи, связанные с автоматизацией Microsoft Excel. Но перед этим мы рассмотрим программные идентификаторы основных объектов Microsoft Excel и объектную модель этого приложения.
184 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Программные идентификаторы и объектная модель Microsoft Excel Существует три типа объектов Excel, которые могут быть созданы непосредст- венно с помощью приложения-контроллера. Эти объекты и соответствующие им программные идентификаторы перечислены в табл. 4.3. Таблица 4.3. Объекты Microsoft Excel, непосредственно доступные приложению-контроллеру Объект Программный идентификатор Комментарий Application Excel.Application, С помощью этого программного Excel.Application.9 (10) идентификатора создается экземпляр приложения без открытых рабочих книг WorkBook Excel.Addin С помощью этого программного идентификатора создается экземпляр расширения (add-in) Excel (имеющиеся расширения доступны с помощью команды Tools ► Add-Ins) Excel.Chart, Excel.Chart.8 Рабочая книга, созданная с помощью этих программных идентификаторов, состоит из двух листов — одного для диаграммы, другого — для данных, па основе которых опа построена Excel.Sheet, Excel.Sheet.8 Рабочая книга, созданная с помощью этих программных идентификаторов, состоит из одного листа Все остальные объекты Excel являются внутренними. Небольшой фрагмент объектной модели Microsoft Excel изображен па рис. 4.2. Основным в объектной модели Excel является объект Application, содержа- щий коллекцию Workbooks объектов типа WorkBook. Каждый объект тина WorkBook содержит коллекцию Worksheets объектов типа Worksheet, коллекцию Charts объ- ектов типа Chart и др. Манипуляция рабочими книгами, их листами, ячейками, диаграммами реально осуществляется путем обращения к свойствам и методам этих объектов. Ниже мы рассмотрим наиболее часто встречающиеся задачи, связанные с авто- матизацией Microsoft Excel. Если вам встретилась задача, не совпадающая пи с од- ной из перечисленных, вы можете попытаться найти подходящий пример па Visual Basic в справочном файле VBAXL9.CHM (или VBAXL10.CHM) либо, как и в случае Microsoft Word, записать соответствующий макрос и проанализировать его код.
Автоматизация Microsoft Excel 185 | | — коллекции и объекты 1 — объекты Рис. 4.2. Фрагмент объектной модели Microsoft Excel Запуск Microsoft Excel, создание и открытие рабочих книг При разработке примеров использования Microsoft Excel можно задействовать код создания контроллера, приведенный в разделе «Общие принципы создания контроллеров автоматизации Microsoft Office», заменив первую строку кода в при- веденном там примере следующей: AppProgID := 'Excel.Application': Кроме того, нужно заменить комментарии кодом, манипулирующим свой- ствами и методами объекта Excel .Application. Отметим, однако, что подключе- ние контроллера автоматизации к имеющейся версии Excel с помощью метода GetActiveOl eObject может привести к Тому, что вся клиентская часть Excel ока- жется невидимой (это происходит, если имеющаяся копия Excel запущена в ре- жиме, когда ее пользовательский интерфейс недоступен). Причины подобного поведения нам неизвестны. Однако, коль скоро такая ситуация возможна, лучше упростить код создания контроллера и всегда создавать новую копию Excel. Изучение темы разработки контроллеров Excel мы начнем с создания и от- крытия рабочих книг. Создать новую рабочую книгу Excel можно, используя метод Add коллекции Workbooks объекта Application: App.WorkBooks.Add;
186 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Для создания рабочей книги па основе шаблона следует указать его имя в ка- честве первого параметра метода Add: Арр.WorkBooks.Add('C:\Program Ft I es\' + ’Microsoft 0ffice\Templates\1033\invoice.xlt'); В качестве первого параметра этого метода можно также использовать сле- дующие константы: const xlWBATChart = JFFFFEFF3; // рабочая книга состоит 11 из листа с диаграммой xlWBATWorksheet = SFFFFEFB9: // рабочая книга состоит И из листа с данными В этом случае рабочая книга будет содержать один лист типа, заданного ука- занной константой (график, обычный лист с данными и др.). Для открытия существующего документа следует воспользоваться методом Open коллекции WorkBooks: Арр.Documents.Open(’0:\МуExcelFile.xls’): Отметим, что свойство ActiveWorkBook объекта Excel .Application указывает па текущую активную рабочую книгу среди одной или нескольких открытых. По- мимо этого, к рабочей книге можно обращаться по ее порядковому номеру; па- пример, ко второй открытой рабочей книге можно обратиться так: Арр.WorkBooks[2] ВНИМАНИЕ -------------------------------------------------------------------- Помните что в Delphi при позднем связывании синтаксис обращения к членам кол- лекций объектов Excel отличен от синтаксиса обращения к объектам Word — в случае Word мы использовали метод Item, а в случае Excel мы обращаемся к членам коллек- ции как к элементам массива. Сделать рабочую книгу активной можно с помощью метода Activate: Арр.WorkBooks[2].Activate: Следующее, что необходимо научиться делать, — это сохранять рабочие книги в файлах. Сохранение, печать и закрытие рабочих книг Microsoft Excel Закрытие документа может быть осуществлено с помощью метода Cl ose одним из следующих способов: Арр.WorkBooks[2].Close: Арр.Acti veWorkBook.Close: В случае позднего связывания метод Close имеет несколько необязательных параметров, влияющих на правила сохранения рабочей книги. Первый из пара-
Автоматизация Microsoft Excel 187 метров принимает значения True или False и определяет необходимость сохране- ния изменений, внесенных в рабочую книгу. Второй параметр (типа Variant) — это имя файла, в котором нужно сохранить рабочую книгу (если в нее были внесены изменения). Третий параметр, также принимающий значения True или False, определяет необходимость пересылки документа следующему пользователю по электронной почте и может быть проигнорирован, если эта функциональность не требуется. App.ActiveWorkBook.CloseCTrue.'C:\MyWorkBook.xls'): Просто сохранить рабочую книгу, не закрывая ее, можно с помощью метода Save: Арр.Acti veWorkBook.Save: Используется также метод SaveAs: Арр.ActiveWorkBook.SaveAs('C:\MyWorkBook.xls'); Метод SaveAs имеет более десятка параметров, влияющих на то, как именно сохраняется документ (под каким именем, с паролем или без него, какова кодо- вая страница для содержащегося в пей текста и др.). Закрыть само приложение Excel можно с помощью метода Quit объекта Excel. Application. В случае Excel этот метод параметров не имеет. Вывод документа Excel на устройство печати можно осуществить с помощью метода Printout объекта WorkBook, например: Арр.Acti veWorkBook.Pri ntOut: Если нужно изменить параметры печати, следует указать значения соответст- вующих параметров метода Pri ntOut (в случае Excel их восемь). Обращение к листам и ячейкам Обращение к листам рабочей книги производится с помощью коллекции Work- Sheets объекта WorkBook. Каждый член этой коллекции представляет собой объект Worksheet. К члену этой коллекции можно обратиться по его порядковому номеру, например: Арр.WorkBooks[l].WorkSheets[l],Name := 'Страница 1': Приведенный выше пример иллюстрирует, как можно изменить имя листа рабочей книги. К листу рабочей книги можно обратиться и по имени, например: Арр.WorkBooks[l].WorkSheets['Sheetl'].Name := 'Страница 1'; Обращение к отдельным ячейкам листа производится с помощью коллекции Cells объекта Worksheet. Например, добавить данные в ячейку В1 можно следую- щим образом: App.WorkBooks[l].WorkSheets['Sheetl'].Cells[l,2].Value := '25';
188 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Здесь первая из координат ячейки указывает на номер строки, вторая — па номер столбца. Добавление формул в ячейки производится аналогичным способом: App.WorkBooksEl].WorkSheetsE'Sheet1' ]. CellsE3.2].Value : = '=SUM(B1:B2)': Очистить ячейку можно с помощью метода ClearContents. Форматирование текста в ячейках производится с помощью свойств Font и Interior объекта Cell и их вложенных свойств. Например, следующий фрагмент кода выводит текст в ячейке красным полужирным шрифтом Courier кегля 16 на желтом фоне. App.WorkBooksEl].WorkSheetsEl].Cells[3,2].Interior.Col or := cl Yellow: App.WorkBooksEl].WorkSheetsEl].CellsE3,2].Font.Color := clRed; App.WorkBooksEl].WorkSheetsEl].CellsE3.2].Font.Name := 'Courier'; App.WorkBooksEl].WorkSheetsEl].CellsE3.2].Font.Size := 16: App.WorkBooksEl].WorkSheetsEl].CellsE3.2].Font.Bold := True; Вместо свойства Color можно использовать свойство Col or Index, принимающее значения от 1 до 56; таблицу соответствий значений этого свойства реальным цветам можно найти в справочном файле VBAXL9.CHM или VBAXL10.CHM. Обратиться к текущей ячейке можно с помощью свойства Acti veCel 1 объекта Excel .Application, а узнать местоположение ячейки — с помощью свойства Address объекта Cel 1, например: ShowMessage(App.ActiveCell.Address); Помимо обращения к отдельным ячейкам, можно манипулировать прямо- угольными областями ячеек с помощью объекта Range, например: Арр.WorkBooks[l].WorkSheets[2].Range['Al:C5'].Value := 'Test': Арр.Work Book s Е1].Worksheets[2].RangeE’Al:C5'].Font.Col or := clRed: Приведенный выше код приводит к заполнению прямоугольного участка тек- стом и изменению цвета шрифта ячеек. Объект Range также часто используется для копирования прямоугольных об- ластей через буфер обмена. Ниже приведен пример, иллюстрирующий такую возможность: Арр.Wo гk Book s Е1].WorkSheetsE2].RangeE'Al:C5'].Copy: App. WorkBooksEl]. Worksheets E 2]. RangeE' All: C15' ]. Sei ect; App.WorkBooksEl].WorkSheetsE2].Paste: Обратите внимание на то, что диапазон, куда копируются данные, предвари- тельно выделяется с помощью метода Select. Отметим, что примерно таким же образом можно копировать данные и из других приложений (например, из Microsoft Word). Довольно часто при автоматизации Excel используются возможности этого приложения, связанные с построением диаграмм. Ниже мы рассмотрим, как это сделать.
Автоматизация Microsoft Excel 189 Создание диаграмм Диаграммам Excel соответствует объект Chart, который может располагаться как на отдельном листе, так и па листе с данными. Если объект Chart располагается на листе с данными, ему соответствует член коллекции Chartobjects объекта Worksheet, и создание диаграммы нужно начать с добавления элемента в эту коллекцию: Ch := Арр.Work Books[1].WorkSheets[2].Chartobjects.Add(10,50.400.400); Параметрами этого метода являются координаты левого верхнего угла и раз- меры диаграммы в пунктах (1/72 дюйма). Если же диаграмма располагается иа отдельном листе (не предназначенном для храпения данных), то ее создание нужно начать с добавления элемента в кол- лекцию Sheets объекта Application (отличающуюся от коллекции Worksheets тем, что она содержит листы всех типов, а не только листы с данными): Арр.WorkBooks[l].Sheets.Add( , .1 .xlWBATChart): В этом случае первый параметр метода Add идентифицирует порядковый по- мер листа, перед которым нужно поместить лист с диаграммой (или листы, если их несколько), второй параметр — порядковый помер листа, после которого нужно поместить лист с диаграммой (используется обычно один из них), третий параметр — количество создаваемых листов, а четвертый — их тип. Значения четвертого параметра совпадают со значениями первого параметра метода Add коллекции WorkBooks объекта Application, и при использовании имен соответст- вующих констант следует определить их в приложении-контроллере. Простейший способ создать диаграмму с точки зрения пользователя — по- строить ее с помощью соответствующего мастера на основе прямоугольной об- ласти с данными. Точно так же можно создать диаграмму и с помощью контрол- лера автоматизации — для этой цели у объекта Chart, являющегося свойством объекта Chartobject (члена коллекции Chartobjects), имеется метод Chartwizard. Первым параметром этого метода является объект Range, содержащий диапазон ячеек для построения диаграммы, а вторым — числовой параметр, указывающий, какого типа должна быть эта диаграмма: var Ch: Variant; Ch.Cha rt.Cha rtWi za rd( App. WorkBooks [ 1 ]. Worksheets [ 2]. Range [ ’ Al: C5' ]. xl 3DCol unin): Возможные значения параметра, отвечающего за тип диаграммы, можно найти в справочном файле. У объекта Chart имеется множество свойств, которые отвечают за внешний вид диаграммы и с помощью которых можно изменить ее точно так же, как это де- лают пользователи вручную. Ниже приводится пример создания заголовка диа- граммы и подписей вдоль ее осей (отметим, что оси есть не у всех типов диаграмм). Ch.Chart.HasTitie := 1: Ch.Chart.HasLegend : = False:
190 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Ch.Chart.ChartTitle.Text := 'Пример диаграммы Excel’: Ch.Chart.Axes(l).HasTitle := True: Ch.Chart.Axes(l).AxisTitle.Text := 'Подпись вдоль оси абсцисс': Ch.Chart.Axes(2).HasTitle := True; Ch.Chart.Axes(2).AxisTitle.Text := 'Подпись вдоль оси ординат': Еще один способ создания диаграммы — определить все ее параметры с по- мощью свойств объекта Chart, включая и определение серий, на основе кото- рых она должна быть построена. Данные для серии обычно содержатся в объекте Range, содержащем строку или столбец данных, а добавление серии к диаграмме производится путем добавления члена к коллекции SeriesCollection, напри- мер: Арр.WorkBooks[l].Sheets.Add( , . 1. xlWBATChart): App.WorkBooks[l].Sheetsfl].ChartType := x!3DPie: Rng:=App.WorkBooks[1].Worksheets[2].RangeL'Bl:B5']; App.WorkBooks[1].Sheets[1].Seri esCol1ecti on.Add(Rng): В данном примере к диаграмме, созданной на отдельном листе, специально предназначенном для диаграмм, добавляется одна серия на основе диапазона ячеек другого листа. Применение средств доступа к данным Из коллекций объекта Worksheet, позволяющих обращаться к реляционным и OLAP- данным с помощью средств Microsoft Query, наиболее интересны коллекции QueryTables и PivotTables, а также коллекция PivotCaches объекта Workbook. С по- мощью этих коллекций можно отображать в Excel результаты запросов к базам данных, доступных с помощью механизмов доступа к данным ODBC и OLE DB. Отобразить па рабочем листе Excel результат запроса к базе данных мож- но, добавив новый объект QueryTable к коллекции QueryTables объекта Workbook и установив его свойство CommandText равным тексту запроса, например: var Арр. QTable: Variant: QTabl е:= Арр.WorkBooks[1].Sheets[1].QueryTables.Add (’ODBC;DRIVER=SQL Server;'+ 'SERVER=MAINDESK:UID=Admi ni strator:' + ’APP=Microsoft Office XP;WSID=MAINDESK;'+ ’DATABASE=Northwind:Trusted_Connection=Yes', Ws.Range['Al:Al']); QTable.CommandText := 'SELECT * FROM Customers': QTable.Refresh; При необходимости можно отобразить имена полей, установив соответствую- щее значение свойства FieldNames объекта QueryTable: QTable.FieldNames := True:
Автоматизация Microsoft Excel 191 Можно также привести ширину колонок в соответствие с длиной полей набора данных, полученного в результате запроса: QTable.Adjusted umnWidth := True: Для создания сводных таблиц следует осуществить кэширование просумми- рованных данных в памяти Excel. Для этого перед созданием сводной таблицы нужно создать объект QueryTable PivotCache, добавить его к коллекции PivotCaches объекта Workbook и установить его свойства Connection, CommandType и CommandText, определяющие, откуда берутся исходные данные для сводной таблицы: var WB, PC, PT: Variant: PC := WB.PivotCaches.Add(xlExternal): PC.Connection : = '0LEDB;Provider=Microsoft.Jet.0LEDB.4.0;'+ 'Data Source=C:\data\Northwind.mdb': PC.CommandType := xlCmdSql; PC.CommandText := 'SELECT Country. City. '+ ' ProductName, Salesperson. ExtendedPrice FROM Invoices ': Далее следует создать саму сводную таблицу: PC.CreatePivotTable(WB.Worksheets[l].Cells[l,l], 'PivotTablel'): PT := WB.WorksheetsEl].PivotTables!'PivotTablel'); В этом случае нам потребуются следующие константы: const xlExternal = $00000002;//используются внешние данные xlCmdSql = $00000002; //используется SQL-запрос Можно предоставить конечному пользователю возможность самому опреде- лить, как будут расположены поля сводной таблицы, а можно расположить их программно, воспользовавшись коллекцией PivotFields объекта PivotTable: PT.PIvotFiel ds('Country').Orientation := xlRowField: PT.PivotFields!'ProductName').Orientation := xlPageField: PT.PivotFields!'Salesperson').Orientation := xl Col uninF i eld; PT.PivotFields!'ExtendedPrice').Orientation := xlDataField: В этом случае нам потребуются следующие константы: const xlHidden = $00000000: // поле не используется xlRowField = $00000001; // поле помещается в область строк xlColumnField = $00000002: // поле помещается в область столбцов xlPageField = $00000003: // xlDataField = $00000004; // // поле помещается в область фильтров поле помещается в область суммируемых данных Для чтения OLAP-кубов мы также должны определить, откуда берутся ис- ходные данные, по в свойстве Connection в этом случае указываются параметры доступа к кубу, например:
192 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office PC.Connection := 'OLEDB:Provider=MSOLAP'+ ’ Initial Catalog=[OCWCube]: Data Source=C:\MyCube.cub': PC.CommandType := xlCmdCube: Или (в случае куба, созданного с помощью Microsoft SQL Server 2000 Analysis Services): PC.Connection : = '0LEDB;Provider=MS0LAP.2; '+ ’Data Source=MAINDESK:Initial Catalog=FoodMart 2000:' PC.CommandType := xlCmdCube: Далее нам следует воспользоваться коллекцией CubeFi el ds объекта PivotTable, чтобы определить, какие поля требуются для формирования строк, столбцов, страниц и что нужно отобразить в области данных, например: PT.CubeFiel ds[2].Orientation := xlRowField: PT.CubeFiel ds[5].Orientation := xlColumnField; PT.CubeFields[4],Orientation := xlPageField; PT.CubeFields[6],Orientation := xlDataField: При создании приложения с помощью Delphi обратиться к членам коллекции CubeFi el ds по имени нельзя, поэтому приходится использовать их порядковые номера в коллекции. При необходимости с помощью Excel можно создать и локальный OLAP-куб. В этом случае свойство Connectionstring должно содержать все параметры, необ- ходимые для создания куба и доступа к нему, а именно параметры соединения, указывающие на будущий куб, и источник фактических данных для пего, пред- ложение CREATE CUBE, в котором описываются измерения куба и уровни их иерар- хии, а также меры куба, и предложение INSERT INTO, указывающее, как параметры исходного запроса связаны с метаданными куба. Вопрос создания OLAP-кубов выходит за рамки темы данной книги. Интересующиеся данным вопросом мо- гут найти подробное описание синтаксиса предложений CREATE CUBE и INSERT INTO в разделе SQL Server 2000 Books Online, посвященном программированию Analysis Services. Итак, в данном разделе мы изучили основные операции, которые наиболее часто применяются при автоматизации Microsoft Excel. Описание некоторых опера- ций, аналогичных соответствующим операциям для автоматизации Microsoft Word, например обращение к свойствам документов, здесь были опущены, дабы избежать повторов. Возможности автоматизации Microsoft Excel, как и в случае Microsoft Word, далеко не исчерпываются приведенными примерами, по при необходимости сведения о них всегда можно найти в соответствующем справочном файле. Автоматизация Microsoft PowerPoint В данном разделе мы обсудим наиболее часто встречающиеся задачи, связанные с автоматизацией Microsoft PowerPoint. Но перед этим мы рассмотрим программ- ные идентификаторы основных объектов Microsoft PowerPoint и объектную модель этого приложения.
Автоматизация Microsoft PowerPoint 193 Программные идентификаторы и объектная модель Microsoft PowerPoint Для приложения-контроллера доступен непосредственно только один объект Application, программным идентификатором которого является PowerPoint.Application или PowerPoint.Application.9 (10). С помощью этого программного идентификатора создается экземпляр PowerPoint без открытых презентаций. Все остальные объекты PowerPoint являются внутренними. Это означает, что они не могут быть созданы сами по себе; так, объект Presentation (презентация) не может быть создан отдельно от самого приложения. Небольшой фрагмент объектной модели Microsoft PowerPoint изображен на рис. 4.3. 2] — коллекции и объекты [3 — объекты Рис. 4.3. Фрагмент объектной модели Microsoft PowerPoint Основным в объектной модели PowerPoint является объект Application, содер- жащий коллекцию Presentations объектов типа Presentation. Каждый объект типа Presentation содержит коллекцию Slides объектов типа Slide, соответствующих слайдам презентации. Слайды, в свою очередь, содержат коллекции Shapes типа
194 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Shape, соответствующие элементам слайдов презентации. Манипуляция презен- тациями, слайдами и их элементами реально осуществляется путем обращения к свойствам и методам этих объектов. Ниже мы рассмотрим наиболее часто встречающиеся задачи, связанные с ав- томатизацией Microsoft PowerPoint. Если вам встретилась задача, не совпадаю- щая пи с одной из перечисленных, вы можете попытаться найти подходящий пример на Visual Basic в справочном файле VBAPPT9.CHM (или VBAPP10.CHM) либо, как и в случае Microsoft Word или Microsoft Excel, записать соответствую- щий макрос и проанализировать его код. Запуск Microsoft PowerPoint, создание и открытие презентаций В плане разработки примеров использования Microsoft PowerPoint можно задей- ствовать код создания контроллера из раздела «Общие принципы создания кон- троллеров автоматизации Microsoft Office», заменив комментарии кодом, мани- пулирующим свойствами и методами объекта PowerPoint.Application. Изучение темы разработки контроллеров PowerPoint мы начнем с создания и открытия презентаций. Создать новую презентацию PowerPoint можно, используя метод Add коллек- ции Presentations объекта Application: Арр.Presentati ons.Add: Для открытия уже существующей презентации документа следует воспользо- ваться методом Open коллекции Presentations: Арр.Presentati ons Open('С:\MyPresentation.ppt’); Отметим, что свойство ActivePresentation объекта PowerPoint.Application ука- зывает на текущую активную презентацию среди одной или нескольких откры- тых. Помимо этого, к рабочей книге можно обращаться по ее порядковому но- меру; например, ко второй открытой рабочей книге можно обратиться так: Арр.Presentations.Itern(2) Обратите внимание, что в Delphi при позднем связывании синтаксис обраще- ния к членам коллекций объектов PowerPoint аналогичен синтаксису обращения к объектам Word (и, соответственно, отличен от синтаксиса обращения к объек- там Excel). Отметим также, что в случае PowerPoint, в отличие от Word и Excel, объект Application не имеет метода Activate, с помощью которого можно было бы сде- лать активной конкретную презентацию среди нескольких открытых. Для реше- ния этой задачи следует обращаться к коллекции Windows объекта Presentation или к объектам DocumentWindow и SlideShowWindow, например: Арр.Presentations.11еш(1).Windows.Itern(1).Act 1 vate; Следующее, чему необходимо научиться — это сохранять презентации в файлах.
Автоматизация Microsoft PowerPoint 195 Сохранение, печать и закрытие презентаций Microsoft PowerPoint Закрытие презентации может быть осуществлено с помощью метода Cl ose одним из следующих способов: Арр. Presentati ons. I tern (2). Cl ose; App.Acti vePresentati on.Cl ose: Обратите внимание, что в случае PowerPoint метод Close закрывает презента- цию, пе предлагая пользователю сохранить изменения, и пет параметров, с помо- щью которых можно было бы повлиять па возможность сохранения изменений (вспомним, что в случае Word и Excel этот метод обладал параметрами, влияю- щими па возможность сохранения документа перед его закрытием). Для сохранения презентации следует воспользоваться методом Save или SaveAs: Арр.Presentati ons.11 em(2).Save: App.Presentati ons.Item(2).SaveAs(’C:\MyPresl.ppt’): В общем случае метод SaveAs имеет три параметра, влияющих на конкретный способ сохранения презентации. Первый из них (обязательный) представляет собой строку, содержащую имя файла, в котором сохраняется презентация. Если в этой строке путь к файлу пе указан, файл сохраняется в текущем каталоге. Второй параметр этого метода (необязательный в случае позднего связыва- ния) указывает, в каком формате сохраняется презентация. Это целый параметр, принимающий следующие значения: const ppSaveAsPresentati on = $00000001: // формат текущей И версии PowerPoint ppSaveAsPowerPoi nt7 ppSaveAsPowerPoint4 ppSaveAs PowerPoi nt3 ppSaveAsTemplate ppSaveAsRTF ppSaveAsShow = $00000002; // формат PowerPoint 7 = $00000003; // формат PowerPoint 4 = $00000004: // формат PowerPoint 3 = $00000005; // сохранить как шаблон = $00000006: // формат RTF = $00000007: // формат SlideShow И (*.pps) ppSaveAsAddin = $00000008; // формат PowerPoint // Addin (*.рра) ppSaveAsPowerPoint4FarEast = SOOOOOOOA: // формат PowerPoint 4 ppSaveAsDefault ppSaveAsHTML ppSaveAsHTMLv3 ppSaveAsHTMLDual // Far East (версия для Китая, Японии, // стран Юго-Восточной Азии) = $00000008: // формат по умолчанию = $00000000: // формат HTML = $00000008; // формат HTML 3 = $0000000Е; // формат HTML для ppSaveAsMetaFile ppSaveAsGIF II текстов в двухбайтовой кодировке = $0000000F; // формат WMF = $00000010; // формат GIF
196 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office ppSaveAsJPG ppSaveAsPNG ppSaveAsBMP = $00000011 = $00000012 = $00000013 // формат JPG И формат PNG 11 формат BMP Третий параметр указывает, внедрять ли в презентацию используемые в пей шрифты. Ои может принимать значения True (внедрять шрифты) и False (не внедрять шрифты); по умолчанию используется значение False. Закрыть само приложение PowerPoint можно с помощью метода Quit объекта PowerPoint! .Application. В случае PowerPoint этот метод параметров не имеет. Вывод документа PowerPoint на устройство печати можно осуществить с по- мощью метода Printout объекта Presentation, например: Арр.Presentations.Item(2).PrintOut; Если нужно изменить параметры печати, следует указать значения соответст- вующих (необязательных при позднем связывании) параметров метода Printout. В случае PowerPoint их пять. Первые два целых параметра определяют началь- ный и конечный печатаемые слайды, третий параметр представляет собой строку с именем файла, если вывод происходит в файл, а не на принтер, четвертый (целый) параметр определяет количество печатаемых экземпляров. Пятый пара- метр указывает, должен ли быть напечатанный документ разобран по экземпля- рам, и принимает значение True (по умолчанию) или False. Например, для вывода с пятого по двадцатый слайды в файл C:\MyOutput.prn в трех экземплярах можно использовать следующий код: Арр.Presentations.Itern!2).Printout! 5, 20. 'c:\MyOutput.prn', 3): Отметим, однако, что, помимо параметров метода Printout, влияющих на то, какой принтер используется и сколько экземпляров печатается, па режим печати презентации влияет также свойство PrintOptions объекта Presentation. Это свойство представляет собой объект PrintOption, имеющий, в свою очередь, набор свойств, влияющих па то, в каком виде печатается презентация (слайды, выдачи, заметки и др.), печатаются ли рамки вокруг слайдов, сколько слайдов на странице распо- лагается при печати выдач, печатается ли фон слайдов и др. Этот набор свойств примерно отражает то, что пользователь PowerPoint может изменить, выбрав ко- манду File ► Print в меню PowerPoint (в том числе и имя принтера, и имя презен- тации, и число копий). В следующем разделе мы поговорим о том, как изменить оформление презен- тации. Оформление презентаций Для оформления презентации обычно применяются цветовые схемы, шаблоны и об- разцы. Применить шаблон к презентации можно с помощью метода ApplyTempl ate объекта Presentation: Арр.Presentat1ons.Item!1).ApplyTemplate(’C:\Program Files'-*- '\Microsoft Office\Templates\Presentation DesignsV + 'Bamboo.pot'):
Автоматизация Microsoft PowerPoint 197 Отметим, что если до присоединения шаблона к слайдам презентации были применены стандартные цветовые схемы (о них речь пойдет чуть позже), после присоединения шаблона эти цветовые схемы будут утеряны. Для получения образцов слайдов, титульного слайда, раздаточных материалов и заметок используются свойства SI i deMaster, Ti tl eMaster, HandoutMaster и NotesMaster объекта Presentation. К образцам можно применять фоновую заливку, добав- лять графические объекты и элементы управления ActiveX, изменять оформле- ние текста. Для изменения цветовой схемы в образце слайдов, титульного слайда, разда- точных материалов или заметок следует воспользоваться свойством Col orScheme объекта Master. Свойство Col orScheme возвращает объект Col orScheme, содержащий коллекцию Colors из восьми цветов. Элементы этой коллекции соответствуют следующим элементам цветового оформления: const ppBackground = $00000001: // ppForeground = $00000002: // ppShadow = $00000003: // ppTitle = $00000004; // ppFill = $00000005: // ppAccentl = $00000006; // ppAccent2 = $00000007: // // ppAccent3 = $00000008: // // цвет фоне цвет текста и границ автофигур цвет тени цвет текста заголовка цвет заливки автофигур цвет, применяемый в диаграммах цвет, применяемый в диаграммах и гипертекстовых ссылках цвет, применяемый в диаграммах и выбранных гипертекстовых ссылках Например, следующий фрагмент кода устанавливает для слайдов презента- ции серый фон и темно-красный цвет заголовков: Арр.Presentati ons.Item(1).SIi deMaster.ColorScheme. Colors(ppBackGround).RGB := RGB(128. 128. 128); App.Presentati ons.Item(1).SIi deMaster.ColorScheme. Colors(ppTitle).RGB : = RGB(200, 0. 0); Можно также использовать стандартные цвета, определенные в Delphi. Напри- мер, следующий фрагмент кода устанавливает желтый цвет заливки автофигур: Арр.Presentations.Item(1).SIideMaster.ColorScheme. Colors(ppFill).RGB := clYellow: Для добавления объектов в образцы слайдов следует обращаться к коллекции Shapes объекта Master. Следующий фрагмент кода содержит пример добавления прямоугольника размером 100x200 пикселов в образец слайдов па расстоянии 50 пикселов от левого и верхнего краев слайда: const msoShapeRectangle = $00000001; Арр.Presentati ons.11 em(1).SIi deMaster.Shapes. AddShape(msoShapeRectangle, 50. 50. 100. 200);
198 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Возможные значения параметра, отвечающего за тип автофигуры, можно найти в справочном файле (или в интерфейсном модуле, сгенерированном для библиотеки MSO9.DLL или MSO10.DLL). Для изменения оформления текста в презентации следует обратиться к свой- ству Textstyles объекта Master, содержащему три объекта типа Textstyle (для тек- ста заголовков, основного и стандартного текстов): const ppDefaultStylе = $00000001; // стандартный текст ppTitleStyle = $00000002; // текст заголовков ppBodyStyle = $00000003; // основной текст Объект Textstyle обладает свойствами TextFrame (содержит сведения о распо- ложении текста внутри текстового поля) и Ruler (содержит сведения о позициях табуляции и сдвигах между различными уровнями). Свойство Levels объекта Textstyle содержит пять объектов TextStyleLevel со сведениями об оформлении текста разных уровней. Следующий фрагмент кода устанавливает отступ для ос- новного текста от левого края содержащего его объекта равным 10 пунктов, раз- мер шрифта первого уровня основного текста равным 42 пунктам, а имя этого шрифта равным значению Courier: Арр.Presentations.11em(1).SIi deMaster.TextStyles. Item(ppBodyStyle).TextFrame.MarginLeft : = 10; App.Presentati ons.Item(1).SIi deMaster.TextStyles. Item(ppBodyStyle).Levels.Item(l).Font.Size := 42; App.Presentati ons.Item(1).SIi deMaster.TextStyl es. Item(ppBodyStyle).Levels.Item(l).Font.Name := 'Courier'; Отметим, однако, что, помимо применения цветовых схем, шаблонов и образ- цов ко всей презентации, PowerPoint позволяет создавать оформление для от- дельных слайдов. Об этом мы поговорим в следующем разделе. Манипуляция отдельными слайдами Доступ к слайдам презентации осуществляется с помощью коллекции Slides объ- екта Presentation. Эта коллекция содержит объекты типа Slide, каждый из кото- рых соответствует отдельному слайду презентации. Добавить к презентации новый слайд можно с помощью метода Add коллек- ции Slides. Этот метод имеет два обязательных целых параметра. Первый из них указывает, каким по счету в презентации должен быть вставляемый слайд (это число пе должно превышать текущее число слайдов плюс один), второй пред- ставляет собой номер образца в галерее слайдов. Следующий фрагмент кода ил- люстрирует, как можно добавить слайд, содержащий диаграмму, так чтобы он был вторым в презентации: const ppLayoutChart = $00000008; Арр.Presentati ons.Item(1).SIi des.Add(2.ppLayoutCha rt):
Автоматизация Microsoft PowerPoint 199 Список возможных значений второго параметра можно найти в справочном файле или интерфейсном модуле. Обратиться к слайду можно по его номеру, например: Арр.Presentations.Item(l).Slides.Item(l) Так как порядковые номера слайдов в процессе редактирования презентации могут меняться, для обращения к слайду можно также пользоваться уникаль- ным идентификатором SlidelD, являющимся свойством объекта Slide. В этом случае для поиска слайда можно использовать метод FindBySlidelD: var MylD: integer: const ppLayoutText = $00000002: MylD := App.Presentations.Item(l).Slides.Item(2).SlidelD; App.Presentations.Item(l).SI ides.FindBySlidelD(MylD). Layout := ppLayoutText; Отметим, что при копировании слайда в другую презентацию его идентифи- катор SlidelD изменяется. Свойство Layout объекта Slide, использованное в приведенном примере, по- зволяет изменить тип слайда. Для изменения цветового оформления слайда можно использовать свойство ColorScheme объектов Slide и SlideRange соответственно. Это свойство возвращает объект Col orScheme, описанный выше в разделе «Оформление презентаций». Сле- дующий пример иллюстрирует, как установить синий цвет заголовка второго слайда презентации: Арр.Presentati ons.Itern(1).SIi des.Item(2).ColorScheme. Colors(ppTitle).RGB:= clBlue; Добавление объектов к слайду осуществляется с помощью коллекции Shapes, также описанной в разделе «Оформление презентаций». Свойства каждого объ- екта слайда можно изменять, обращаясь к членам этой коллекции. Каждый объ- ект в слайде имеет уникальный помер и уникальное имя. Имя присваивается при создании объекта по умолчанию, однако его можно заменить чем-нибудь ос- мысленным, например: Арр.Presentati ons.Item(1).SIi des.Item(2).Shapes. Item(l).Name := 'MyText'; Поскольку наиболее часто в слайдах приходится добавлять в объекты текст, остановимся на этом подробнее. Для добавления текста к объекту Shape следует использовать его свойство TextFrame. Следующий пример иллюстрирует добавле- ние текста заголовка слайда, имя которого было заменено выше:
200 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Арр.Presentati ons.Item(1).SI ides.Item(2).Shapes. Item('MyText').TextFrame.TextRange.Text := 'Это заголовок слайда': Для добавления перечислений в текст между строками текста следует встав- лять символы конца строки: Арр.Presentations.Item(l).Slides. Item(2).Layout : = ppLayoutText: App.Presentations.Item(l).SI ides.Item(2).Shapes. Item(2).TextFrame.TextRange.Text := 'Текст l'#13#10'Текст 2’: Для изменения расположения объектов друг поверх друга можно использо- вать свойство Zorder объекта Shape, а для изменения типа заливки — свойство Fill того же объекта. Подробности об использовании этих свойств и список со- ответствующих констант можно найти в справочном файле. Научившись программно создавать презентации и манипулировать слайдами и их объектами, мы можем перейти к автоматизации показа презентаций. Об этом пойдет речь в следующем разделе. Демонстрация слайдов Для показа слайдов используется метод Run объекта SlideShowSettings, являюще- гося свойством объекта Presentation: Арр.Presentati ons.Itern(1).SIideShowSetti ngs.Run; Для установки режима показа слайдов используется тот же самый объект SlideShowSettings. Его свойство RangeType указывает, какой именно фрагмент пре- зентации нужно демонстрировать. Возможные значения этого свойства следующие: const ppShowAl1 = $00000001: // вся презентация ppShowSlideRange = $00000002: // выделенный диапазон // слайдов ppShowNamedSHdeShow = $00000003; // именованная демонстрация Свойства StartingSlide и EndingSlide объекта SlideShowSettings содержат номера первого и последнего слайда демонстрируемого фрагмента. Эти свойства имеет смысл применять в случае, если свойство RangeType этого же объекта равно ppShowSlideRange. Свойство AdvanceMode объекта SlideShowSettings указывает, каким образом про- изводится смена слайдов при демонстрации: const ppSlideShowManualAdvance = $00000001; // ручная смена И слайдов ppSlideShowllseSlideTimings = $00000002: // смена слайдов // в соответствии со временем II показа каждого слайда ppSHdeShowRehearseNewTimings = $00000003: // запись времени И показа слайдов
Автоматизация Microsoft PowerPoint 201 Свойство LoopUnti I Stopped объекта SlideShowSettings, принимающее значения True или False, указывает, демонстрируются ли слайды непрерывно до нажатия пользователем клавиши Esc. Это свойство имеет смысл применять, если свойство AdvanceMode объекта SlideShowSettings установлено равным ppSlideShowUseSlideTifirings. Отметим также, что значения времени показа слайдов, если таковые не записаны вручную пользователем, также можно установить программно с помощью свой- ства SlideShowTransition объекта Slide, например: Арр.Presentations.Item(2).SI ides.Item(5). SlideShowTransition.AdvanceTime := 1: App.Presentati ons.Item(2).SI ides.Item(5). SlideShowTransition.AdvanceOnTime := True: Следующий пример иллюстрирует применение описанных выше свойств и ме- тодов. for I := 2 to 7 do begin App.Presentati ons.Item(1).SIi des.Item(i). SlideShowTransition.AdvanceTime := 1: App.Presentations.Item(l).SI ides.Item(i). SlideShowTransition.AdvanceOnTime := True; end: App.Presentations.Item(l).SlideShowSettings.StartingSlide := 2; App.Presentations.Item(l).SlideShowSettings.EndingSlide := 7; App.Presentati ons.Item(1).SIi deShowSetti ngs.AdvanceMode := ppSli deShowUseSlideT i mi ngs: App.Presentati ons.Item(1).SIi deShowSetti ngs. LoopUnti1 Stopped := True: App.Presentations.Item(1).SIideShowSetti ngs.RangeType := ppShowSlideRange: App.Presentati ons.Item(1).SI 1deShowSetti ngs.Run; В приведенном фрагменте кода время показа слайдов со второго по седьмой устанавливается равным одной секунде, далее указывается, что будут демонстриро- ваться слайды со второго по седьмой в соответствии с заданным временем непре- рывно до нажатия пользователем клавиши Esc, а затем запускается показ слайдов. Свойство SlideShowTransition объекта Slide может быть использовано для оп- ределения анимационных эффектов при смене слайдов. Следующий фрагмент кода иллюстрирует, каким образом можно установить анимационные эффекты и звуковое сопровождение при смене слайда: const ppEffectBlindsVertical = $00000302: App.Presentations.Item(l).SI ides.Item(2). SlideShowTransition.EntryEffect := ppEffectBlindsVertical: App.Presentations.Item(1).SI ides.Item(2). SIi deShowT ransiti on.SoundEffeet.ImportFromFi 1 e ('C:\Program Files\NetMeeting\blip.wav');
202 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Константы, соответствующие различным эффектам, можно найти в интер- фейсном модуле или справочном файле. И, наконец, кратко остановимся на управлении поведением объектов при по- казе слайдов. Для управления анимацией объектов на слайдах следует использо- вать свойство Animationsettings объекта Shape. Это свойство возвращает объект Animationsettings, свойства которого и отвечают за анимацию данного объекта. Например, свойство AdvanceMode определяет способ появления объекта, а свой- ство AdvanceTime — время, через которое после показа слайда должен появиться объект. Свойство TextLevel Effect задает уровень текста, до которого происходит анимация объекта. И наконец, свойство Animate (принимающее значения False или True) указывает, должен ли вообще объект отображаться с анимацией. При- мер управления поведением объекта па слайде приведен ниже: const ppAnimateByAllLevels = $00000010: ppAdvanceOnTime = $00000002: App.Presentations.Item(1).SI ides.Item(3).Shapes.Item(1). Animationsettings.AdvanceMode := ppAdvanceOnTime: App.Presentations.Item(l).SI ides.Item(3).Shapes.Item(l). AnimationSettings.AdvanceTime := 1; App.Presentations.Item(l).SI ides.Item(3).Shapes.Item(l). Animationsettings.TextLevelEffect := ppAnimateByAllLevels: App.Presentations.Item(l).Slides.Item(3).Shapes.Item(1). Animationsettings.Animate := True; Приведенный выше фрагмент кода устанавливает автоматическую анимацию первого объекта па третьем слайде через одну секунду после показа слайда. Итак, в данном разделе мы изучили основные операции, которые наиболее часто применяются при автоматизации Microsoft PowerPoint. Естественно, воз- можности автоматизации этого сервера далеко не исчерпываются приведенными примерами, по при необходимости сведения о них всегда можно найти в соответ- ствующем справочном файле. Описание некоторых операций, аналогичных со- ответствующим операциям при автоматизации Microsoft Word и Excel, напри- мер обращение к свойствам документов, здесь было опущено с целью избежать повторов. Следующим приложением Microsoft Office, автоматизацию которого мы рас- смотрим, будет Microsoft Outlook — один из самых популярных па сегодняшний день персональных информационных менеджеров (Personal Information Manager, PIM). Автоматизация Microsoft Outlook В данном разделе мы обсудим наиболее часто встречающиеся задачи, связанные с автоматизацией Microsoft Outlook. Но перед этим мы рассмотрим программные идентификаторы основных объектов Microsoft Outlook и объектную модель этого приложения.
Автоматизация Microsoft Outlook 203 Программные идентификаторы и объектная модель Microsoft Outlook Для приложения-контроллера доступен непосредственно один объект Application, программный идентификатор которого Out 1 ook. Appl i cati on или Outl ook. Appl i cati on. 9 (10). С помощью этого программного идентификатора создается экземпляр при- ложения. Все остальные объекты Outlook являются внутренними. Небольшой фрагмент объектной модели Microsoft Outlook изображен па рис. 4.4. [2] — коллекции и объекты [Д —объекты Рис. 4.4. Фрагмент объектной модели Microsoft Outlook Основным в объектной модели Outlook является объект Appl i за t i on. В отли- чие от ранее рассмотренных корневых объектов Word.Application, Excel .Application и PowerPoint.Application, объект Outlook.Application не содержит коллекций. Однако с помощью метода GetNameSpace он позволяет получить объект NameSpace, пре- доставляющий доступ к источникам данных. В версии Outlook 2000 поддержи- вается единственный источник данных — MAPI, предоставляющий доступ ко всем данным Outlook. Объект NameSpace, в свою очередь, содержит коллекции Folders объектов типа MAPIFolder, AddressLists объектов типа AddressList и некоторые другие. Каждый объект MAPIFolder (представляющий папку Outlook), в свою оче- редь, содержит коллекцию Items объектов типа Item (элементов, содержащихся в папке, например сообщений), а каждый объект Item содержит несколько других коллекций (например, Attachments, Recipients, Links). Каждый объект AddressList (список адресов) содержит коллекцию Addressitems объектов Addressitem. Ниже мы рассмотрим паибо. tee часто встречающиеся задачи, связанные с ав- томатизацией Microsoft Outlook. Если вам встретилась задача, не совпадающая ни с одной из перечисленных, можно попытаться найти подходящий пример на Visual Basic в справочном файле VBAOUTL9.CHM (VBAOL10.CHM) либо, как и в случае других приложений Microsoft Office, записать соответствующий мак- рос и проанализировать его код.
204 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Запуск Microsoft Outlook, открытие и создание папок При создании примеров использования Microsoft Outlook можно задействовать код создания контроллера, приведенный в начале данной главы, заменив первую строку кода следующей: AppProgID := 'Outlook.Application'; Кроме того, требуется заменить комментарии кодом, манипулирующим свой- ствами и методами объекта Outlook.Application. Добавим также несколько пере- менных для объектов NameSpace и MAPIFolder: var Арр. NS. FLD : Variant: Инициировать создание нового сеанса Outlook можно с помощью метода Logon объекта' NameSpace. Этот метод имеет четыре параметра. Первые два параметра стро- ковые и содержат имя профиля пользователя и его пароль. Третий параметр, при- нимающий значения True или False, указывает, следует ли выводить диалоговое окно ввода имени и пароля, четвертый — создавать ли новый сеанс, например: NS := Арр. GetNamespaceU МАРТ): NS.Logon!'MyProfile'. 'MyPasssword'. False. False): Все эти параметры в случае позднего связывания не обязательны. Так, при отсутствии профилей пользователя можно применить следующий фрагмент кода: NS := Арр.GetNamespaceCМАРТ): NS.Logon; Для завершения работы Outlook следует вызвать метод Logoff объекта NameSpace: NS.Logoff: Арр.Quit: Для открытия папки Outlook следует с помощью метода GetDefaultFolder объ- екта указать, какая папка будет открыта, а затем показать окно Outlook с помо- щью метода Display объекта MAPIFolder: FLD := NS.GetDefaultFolder(olFolderlnbox): FLD.Di splay: Возможные значения параметра метода GetDefaultFolder, соответствующие стандартным, создаваемым по умолчанию папкам Outlook, перечислены ниже: const 01 Fol derDeletedltems = $00000003 // папка Deleted Items olFolderOutbox = $00000004 // папка Outbox olFolderSentMail = $00000005 // папка Sent olFolderInbox = $00000006 // папка Inbox olFolderCalendar = $00000009 u папка Calendar ol FolderContacts = $0000000A // папка Contacts
Автоматизация Microsoft Outlook 205 olFolderJournal olFolderNotes olFolderTasks olFolderDrafts = SOOOOOOOB; = $00000000; = SOOOOOOOD; = $00000010: // папка Journal 11 папка Notes 11 папка Tasks И папка Drafts Стандартные папки могут содержать элементы определенного типа (напри- мер, почтовые сообщения). Подробнее об элементах мы поговорим ниже. Для смены нанки в процессе работы Outlook можно снова вызвать метод GetDefaultFolder объекта NameSpace. Папки Outlook представляют собой иерархию вложенных объектов, поэтому каждый объект MAPIFolder обладает свойством Folders; корневым объектом в этой иерархии является объект NameSpace. Создание новой папки можно осуществить с помощью метода Add коллекции Folders. Например, следующий фрагмент кода создает папку MyBoxl внутри заданной папки: FLD := NS.GetDefaultFolder(olFolderlnbox); FLD.Di splay: FLD.Folders.Add('MyBoxl'); По умолчанию дочерняя (вложенная) папка наследует тип родительской папки. Объект MAPIFolder обладает свойством Parent, указывающим на папку верхнего уровня. Следующий пример кода иллюстрирует создание повой папки па том же уровне иерархии, что и папка, соответствующая переменной FLD: FLD.Ра rent.Fol ders.Add('MyBox2'): Для удаления папки Outlook можно использовать метод Delete объекта MapiFolder: FLD.Folders(’MyBoxl').Delete; Освоив манипуляцию папками, можно рассмотреть и манипуляцию элемен- тами внутри папок. Манипуляция элементами папок Доступ к элементам внутри папок Outlook осуществляется с помощью коллекции Items объекта MAPIFolder. К конкретному элементу можно обращаться по имени, например: FLD.Items!'MIDAS Essential Pack') Либо по порядковому номеру в коллекции: FLD.Items(1) Отметим, что, в зависимости от того, какой папке принадлежат элементы, коллекция Items может содержать различные типы соответствующих папкам объектов. Например, объекты типа Task Item соответствуют задачам, Mail Item — письмам, Contactitem — контактам и др. Они могут иметь различные наборы свойств. Набор свойств для разных типов элементов различен. Некоторые эле- менты могут содержать коллекции других элементов, например, объект Mai litem содержит коллекцию Recipients объектов Contact (адресов электронной почты, куда отправляется сообщение).
206 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Для добавления объекта в папку используется метод Add коллекции Items. Он имеет один необязательный целый параметр, указывающий па тип добавляемого объекта, например: FLD.Iterns.Add(olMa i11tern); Возможны следующие значения этого параметра: const olMailItem = $00000000; // почтовое сообщение olAppointmentltem = $00000001: // встрече olContactltem = $00000002: // контакт olTaskltem = $00000003: // задача olJournal Item = $00000004; // запись в журнале olNoteitem = $00000005: И заметка olPostltem = $00000006; // сообщение для общего доступа Если параметр метода Add коллекции Items не указан, то по умолчанию созда- ется элемент, соответствующий типу родительской папки. Если же папке не при- своен конкретный тип, создается объект Mail Item. Отметим, что если тип создаваемого элемента не соответствует типу роди- тельской папки, такой элемент будет помещен в стандартную папку, соответст- вующую его типу. Создать новый элемент можно также с помощью метода Createltem объекта Application: var МуItem: Variant; Myltem := App.Createltem(olContactltem); В этом случае новый элемент окажется в соответствующей стандартной папке. Печать текста элемента осуществляется обычно с помощью метода Printout, например: FLD.Items!'MIDAS Essential Pack').Printout: Рассмотрев элементы папок в целом, мы можем перейти к обсуждению мани- пуляции элементами конкретных типов. Мы начнем с управления почтовыми сообщениями. Манипуляция сообщениями электронной почты Для доступа к сообщениям электронной почты следует обращаться к коллекции Items соответствующих папок и добавлять в них объекты Мат 1 Item либо пользо- ваться методом Createltem объекта Application, например: var IM: Variant: IM := FLD.Items.Add(olMailItem):
Автоматизация Microsoft Outlook 207 Как известно, почтовое сообщение должно иметь хотя бы одного адресата. Имена адресатов содержатся в коллекции Recipients объекта Mail Item. Добавить адресата электронной почты можно следующим образом: IM.Recipients.AdcK 'Ivanov@ivanovconsulting.ru'): Однако если адресат зарегистрирован в адресной книге, можно добавить его, сославшись не на адрес электронной почты, а на имя, например: IM.Recipients.Add('Ivan Ivanov'); Отметим, однако, что если такое имя пе зарегистрировано в адресной книге, адресат найден не будет, и пользователь получит сообщение об ошибке. Если адресат должен получить копию письма (то есть его адрес должен нахо- диться в поле СС или ВСС), следует изменить свойство Туре соответствующего элемента коллекции Recipients, например: IM.Recipients.Add('Sidorov@ivanovconsulting.ru'): IM.Reci pients.Add('Petrov@ivanovconsulting.ru'): IM.Reci pi ents('Sidorov@i vanovconsulti ng.ru'). Type:=ol CC: IM.Reci pi ents('Petrov@i vanovconsulti ng.ru').Type:=olBCC: Возможные значения этого свойства следующие: const olTo = $00000001: // поле То olСС = $00000002; // поле СС 01ВСС = $00000003; // поле ВСС Для почтовых сообщений это свойство по умолчанию равно olTo. С помощью свойств То, СС и ВСС объекта Mail Item можно определить содержи- мое одноименных полей почтового сообщения. Адрес автора письма содержится в свойстве SenderName. Адрес получателя письма — обязательный атрибут почтового сообщения. Од- нако в подавляющем большинстве случаев такие сообщения обладают также те- мой и текстом. Тема сообщения может быть определена в свойстве Subject объ- екта Mail Item, например: IM.Subject := 'Your last invoice ': Текст сообщения содержится в свойстве Body объекта Mail Item, например: IM.Body := 'Ваш счет оплачен сегодня'#13+ 'С уважением, А.А.Козлов': Допустим и такой вариант: IM.Body := Memol.Text; Для добавления к почтовому сообщению вложений следует использовать кол- лекцию Attachments объекта Mail Item, например: IM.Attachments.Add('С:\screen .zip'):
208 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Для указания степени приоритетности письма можно использовать свойство Importance объекта Mai litem. Помимо этого, можно использовать свойства Sent, Saved, UnRead для определения того, отправлено ли это письмо, сохранено ли, ос- талось ли непрочтенным. Для сохранения письма в папке Drafts используется метод Save, для помеще- ния его в папку Outbox с целью отправки — метод Send, для отображения в стан- дартном окне Outlook — метод Di spl ay. Для ответа па почтовое сообщение можно применить метод Reply объекта Mail Item, возвращающий другой объект Mail Item, например: IM := FLD.Items(l).Reply: IM.Body := 'Спасибо за высланные материалы'#13 + 'С уважением, И.И.Иванов': Для пересылки сообщения другому адресату используется метод Forward, так- же возвращающий объект Mail Item. IM:=FLD.Items!'MIDAS Essential Pack').Forward: IM.Recipients.Add!'Ivan Ivanov'): Как было сказано выше, адреса для отправки сообщений электронной почты обычно содержатся в адресной книге. В следующем разделе мы поговорим о том, как манипулировать контактами, содержащимися в адресной книге (то есть в папке Contacts). Манипуляция контактами Для доступа к контактам следует обращаться к коллекции Items папок, содержа- щих контакты (по умолчанию это папка Contacts). Контакты создаются путем до- бавления в них объектов Contact Item либо с помощью метода Create Item объекта Application, например: var СТ: Variant; СТ := FLD.Items.Add(olContactltem); Набор свойств объекта Contactitem соответствует данным, которые могут быть связаны с конкретным человеком (имя, фамилия, служебный и домашний теле- фоны, адрес электронной почты, домашняя страница, дата рождения, пол, имена детей и др.). Названия свойств соответствуют этим данным: FirstName, LastName, BusinessPhone, HomePhone, Birthday, Gender, Children и т. д. Всего их несколько десят- ков и подробности о них можно найти в справочном файле. Ниже приведен при- мер создания контакта, содержащего сведения об имени, фамилии, компании, адресе электронной почты и мобильном телефоне: СТ.EmaillAddress := 'ivanov@ivanovconsulting.ru': CT.FirstName := 'Иван': CT.LastName := 'Иванов': CT.CompanyName := 'Ivanov Consulting': CT.MobileTelephoneNumber : = '123-4567': CT.Save:
Автоматизация Microsoft Outlook 209 Как видно из представленного выше кода, для сохранения контакта в адрес- ной книге следует применить метод Save объекта Contact Item. Манипуляция заметками и задачами Для доступа к заметкам следует обращаться к коллекции Items папок, содержа- щих заметки (по умолчанию это папка Notes). Заметки создаются путем добавле- ния в эти папки объектов Note Item либо с помощью метода Createltem объекта Application, например: var Note: Variant: Note := Арр.Createltem(olNoteitem): Как и в предыдущих примерах, создав элемент Outlook, нужно определить его свойства, а затем сохранить, например: Note := Арр.Createltem(olNoteitem); Note. Col or := olBlue; Note.Body := 'This is a body of note ': Note.Save: Возможные значения цветов подложки заметок приведены ниже: const olBlue = $00000000: // синий olGreen = $00000001: // зеленый olPink = $00000002: // розовый olYellow = $00000003: // желтый olWhite = $00000004; // белый Для доступа к задачам следует обращаться к коллекции Items папок, содержа- щих задачи (по умолчанию это папка Tasks). Как и в предыдущих случаях, задачи создаются путем добавления в эти папки объектов Task Item либо с помощью метода Createltem объекта Application, например: Task := App.Createltem(olTaskltem); Как и в предыдущих примерах, создав задачу, нужно определить ее свойства (текст, описание, время напоминания о задаче и др.), а затем сохранить. В приве- денном ниже примере создается задача, о которой следует напомнить 6 сентября 2003 г. в 17:30: Task := App.Createltem(olTaskltem); Task.Body := ’Позвонить Сидорову по поводу завтрашней встречи'; Task.Subject := 'Звонок Сидорову'; Task.ReminderSet := True: Task.ReminderTime := StrToDateTimel'06.09.2003 17:30:00'): Task.ReminderPlaySound := True; Task.Save;
210 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Отметим, что в Delphi и C++Builder при определении свойств объектов Outlook, содержащих дату и время, можно использовать переменные типа TDateTime. Манипуляция другими объектами Outlook осуществляется примерно таким же образом; напомним, однако, что набор свойств и методов различных типов объектов пе совпадает. Подробности о манипуляции другими типами объектов можно прочесть в справочном файле. Итак, в данном разделе мы изучили основные операции, которые наиболее часто применяются при автоматизации Microsoft Outlook. Сведения о других возможных операциях при автоматизации Outlook и примеры для Visual Basic for Applications можно найти в соответствующем справочном файле. Ниже мы рассмотрим несколько примеров автоматизации приложений Micro- soft Office. Первый из них реализует решение одной из наиболее часто встречаю- щихся в реальной практике задач, а именно задачи генерации отчетов по базам данных с помощью Microsoft Word и Microsoft Excel. Создание отчетов по базам данных с помощью приложений Office Генерировать отчеты по базам данных с помощью приложений Microsoft Office приходится на практике довольно часто. Причиной этого является желание поль- зователей получать отчеты в виде файла одного из стандартных форматов, иметь возможность редактировать их и обмениваться ими с клиентами и партнерами. Дело в том, что входившие в комплект поставки Delphi вплоть до версии 7 ком- поненты QuickReport обладали весьма ограниченными возможностями, связанными с созданием отчетов в одном из стандартных форматов, и применение вместо них в качестве составной части корпоративного решения приложений Microsoft Office, как правило, уже имевшихся в компании, нередко было предпочтительнее приобретению профессиональных генераторов отчетов типа Crystal Reports (Crystal Decisions). Хотя с появлением в составе Delphi 7 более приемлемого с точки зре- ния функциональности генератора отчетов Rave Reports проблема сохранения отчетов из приложений Delphi в стандартных форматах была частично решена, приложения Microsoft Office по-прежнему широко применяются для этой цели. Ниже мы рассмотрим один из простейших примеров создания отчетов по ба- зам данных с помощью Microsoft Word и Microsoft Excel. Для этой цели созда- дим новый проект, поместим па его форму компонент TADOConnectlon, два компо- нента TADODataSet и три компонента TButton (рис. 4.5). Рис. 4.5. Главная форма приложения для создания отчетов по базам данных
Создание отчетов по базам данных с помощью приложений Office 211 Для создания отчетов мы будем использовать базу данных NorthWind из комплекта поставки Microsoft Access 2000/2002 или Microsoft SQL Server 2000, поэтому следует установить свойство Connectionstring объекта TADOConnection так, чтобы он был связан с этой базой данных, например: Provider = SQLOLEDB.1; Integrated Security = SSPI: Persist Security Info = False: Initial Catalog = Northwind; Data Source = MAINDESK: Connect Timeout = 15: Workstation ID = CHILD. Далее установим свойства CommandType обоих компонентов TADODataSet равными значению cmdText. Свойство CommandText компонента TADODataSetl установим равным: select CustomerlD, CompanyName. City from Customers Свойство CommandText компонента TAD0DataSet2 установим равным: select Country. Customers.CustomerlD. CompanyName. count(Orderld) from Customers. Orders where Customers.CustomerID=Orders.CustomerlD group by Country, Customers.CustomerID. CompanyName order by Country Теперь можно приступать к написанию кода создания отчета. Генерация отчетов с помощью Microsoft Word Ниже мы создадим простейший пример применения Microsoft Word для генера- ции отчетов путем позднего связывания. Реализуем генерацию табличного отчета в обработчике события OnCHck компо- нента Buttonl: procedure TForml.ButtonlClickCSender: TObject): var I. Rent: Integer; begi n Rent := ADODataSetl.RecordCount: // Запускаем Microsoft Word Wd := CreateOleObjectCWord.Application'); // Отображаем на экране окно Microsoft Nord Wd.Visible := True; // Создаем новый документ Wd.Documents.Add: Doc := Wd.Documents.Item(l); // Добавляем новый абзац Doc.Paragraphs.Add; // Меняем его стиль Doc.Paragraphs.Item(l).Style := 'Heading 1';
212 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office // Создаем заголовок отчета Rng := Doc.Range(O); Rng.InsertBefore('The list of customers’); // Создаем заголовки колонок Doc.Paragraphs.Add: Rng.InsertAfterC'CustomerlDiCustomer Name;Country'); // Перемещаемся на первую запись набора данных ADODataSetl.First: for I := 1 to Rent do begin // Добавляем новый абзац Doc.Paragraphs.Add; // Добавляем поля из текущей записи в новый абзац Rng.InsertAter(ADODataSetl.FieldsEO].AsString + ';' + ADODataSetl.Fields[l].AsString + + ADODataSetl.Fi el ds[2].AsStri ng); ADODataSetl.Next: end: // Превращаем текст в таблицу Rng := Doc.RangeCDoc.Paragraphs.Item(3).Range.Start. Doc.Paragraphs.Item(rcnt+3).Range.End): Tbl := Rng.ConvertToTable(':'. rent, 3): // Изменяем размеры колонок таблицы ТЫ .Columns. Item(l) .Width := ТЫ .Columns.Item(l).Width - 80; Tbl.Columns.Item(2).Width := Tbl.Columns.Item(2).Width + 80; ТЫ .Columns. Item(3).Width := Tbl.Columns.Item(3).Width - 40: // Подавляем вывод диагностических сообщений Wd.Di splayAlerts:=False: // Сохраняем документ Doc.SaveAs('d:\Custrep.doc'): // Закрываем Hord и освобождаем ресурсы Wd.Quit; Wd := Unassigned: end; Нам следует также объявить глобальные переменные для объектов Application, Document, Range и Table: var Forml: TForml: // Переменные для объектов Application. Document. // Range и Table Wd. Doc. Rng. Tbl: Variant; Прокомментируем приведенный выше фрагмент кода. Во-первых, мы должны создать копию Microsoft Word, сделать ее видимой и создать новый документ: Wd ;= Created eObject('Word.Application'); 11 Отображаем на экране окно Microsoft Hord Wd.Visible := true:
Создание отчетов по базам данных с помощью приложений Office 213 // Создаем новый документ Wd.Documents.Add; Затем нужно создать заголовок отчета и заголовки колонок будущей таблицы, добавляя соответствующие абзацы и меняя их стили: // Добавляем новый абзац Doc.Paragraphs.Add: // Меняем его стиль Doc.Paragraphs.Item(l).Style := 'Heading 1': // Создаем заголовок отчета Rng := Doc.Range(O); Rng.InsertBeforeC'The list of customers'): // Создаем заголовки колонок Doc.Paragraphs.Add: Rng.InsertAfter('CustomerID:Customer Name;Country'); Затем следует, перемещаясь по записям набора данных, добавить в документ строки, соответствующие этим записям: // Перемещаемся на первую запись набора данных ADODataSetl.First: for I := 1 to rent do begin // Добавляем новый абзац Doc.Paragraphs.Add: // Добавляем поля из текущей записи в новый абзац Rng.InsertAter(ADODataSetl.Fields[0].AsString+';'+ ADODataSetl.Fields[l].ASStri ng+';' + AD0DataSetl.Fields[2].AsString); ADODataSetl.Next: end: Далее мы превращаем набор строк в таблицу Word и изменяем ширину ее ко- лонок так, чтобы корректно отобразить содержащиеся в ней данные: // Превращаем текст в таблицу Rng := Doc.Range(Doc.Paragraphs.Item(3).Range.Start. Doc.Paragraphs.Item(rcnt+3).Range.End); ТЫ := Rng.ConvertToTable('; ’. rent. 3): // Изменяем размеры колонок таблицы ТЫ .Columns. Item(l) .Width: = ТЫ .Columns.Item(l).Width - 80: ТЫ .Columns.Item(2).Width: = ТЫ .Columns.Item(2).Width + 80; ТЫ .Columns.Item(3).Width:= ТЫ .Columns.Item(3) .Width - 40: Теперь нам нужно сохранить документ, подавив при этом вывод диагностиче- ских сообщений Word: Wd.DisplayAlerts := False; Почему нужно избавиться от вывода диагностических сообщений? В общем случае приложения, подобные Word, можно запускать удаленно, например, с по- мощью средств DCOM или универсальных СОМ-клиентов, доступ к которым осуществляется по протоколам TCP/IP или HTTP/HTTPS (подробнее об этом
214 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office будет рассказано в главе И). В этом случае у пользователя не будет возможно- сти отвечать на вопросы диалоговых окон Word — ведь физически он находится па другом компьютере; кроме того, некоторые режимы применения DCOM таковы, что пользовательский интерфейс DCOM-сервера (включая обработку им событий мыши и клавиатуры) может быть просто недоступен никому из пользователей. То есть диалоговое окно, созданное в оперативной памяти, не получит сообще- ния о событии, связанном со щелчком на одной из кнопок этого окна, и не будет закрыто, и у пользователя создастся впечатление, что приложение «зависло». И, наконец, нам следует сохранить документ и освободить ресурсы: // Сохраняем документ Doc.SaveAs('d:\Custrep.doc'): // Закрываем Word и освобождаем ресурсы Wd.Quit; Wd := Unassigned: При выполнении этого приложения создается документ с отчетом но базе данных Northwind (рис. 4.6). Documenll Microsoft Word ALfKI Alfreds Futterkiste Berlin Ana T rujillo Empye dados у helados Antonio Moreno Taquena Around the Hom ЛЙАТЕ AHTON AROlTT Mexico D.F Mexico DJ7 London BERGS BlAUS Berglurds gnabbhop Blauer SeTbehkatessert Lulea Mannheim* BONAP Bon app' Marseille воттм Bottom-boHai Markets Tshwassen B's Beverages Cactus Co midas para llevar London Buenos Aires CACTU Chop-suey'Chinese Comercio Mineiro CHOPS COMMi Bern ~ Sao Paulo CbNSH ConsolidatedHoldings" London DOMON Tju^nondeentiei Nantes Erret Handel* Graz W FOLKO Folies gowmandes F'olkockfaHB " Lille Bracks A-:. The list of customers tJantes Ftarce totalisation \ > DOЭ41 4 - = 5 « g L: Рис. 4.6. Отчет по базе данных, созданный путем автоматизации Microsoft Word
Создание отчетов по базам данных с помощью приложений Office 215 Итак, мы научились создавать отчеты по базам данных с помощью Microsoft Word. Теперь рассмотрим, как то же самое можно сделать с помощью Microsoft Excel. Генерация отчетов с помощью Microsoft Excel В этом разделе мы создадим простейший пример применения Microsoft Excel для генерации отчетов путем позднего связывания. Реализуем генерацию табличного отчета в обработчике события OnClick компонента Button2: procedure TForml.Button2Click(Sender: TObject): var I, Rent: Integer: begin Rent := ADODataSetl.RecordCount; // Запускаем Microsoft Excel Xl := CreateOleObjectC'Excel.Application'); // Отображаем окно Microsoft Excel Xl.Visible := True: // Создаем новую рабочую книгу Xl.WorkBooks.Add: Wb := XL.WorkBooksElJ; Ws := Wb.Worksheets[1]: Ws.Name := 'Customer list': // Создаем заголовок отчета Ws.Cel 1s[l.1] := 'The list of customers'; Ws.Cellsfl.l],Font.Bold := True; Ws.Cellsfl.l].Font.Size : = 16: Ws.Cells[2.1] := 'CustomerlD': Ws.Cells[2,2] := 'Customer Name’; Ws.Cel 1s[2.3] := 'Country': for I:=l to 3 do Ws.Cells[2.i],Font.Bold := True: // Перемещаемся на первую запись набора данных ADODataSetl.First: for I := 1 to Rent do begin // Добавляем значения полей текущей записи в новую строку Ws.Cells[i+2,1] := ADODataSetl.FieldsfO].AsString; Ws.Cells[i+2.2] := ADODataSetl.Fieldsfl].AsString; Ws.Cells[i+2.3] := ADODataSetl.Fields[2].AsString; ADODataSetl.Next: end: // Изменяем ширину колонок Xl .Columnsfl].ColumnWidth:= Xl.Columns[lJ.Columnwidth + 5: Xl.Columns[2].ColumnWidth:= Xl.Columns[2].Columnwidth + 30: Xl .Columns[3].ColumnWidth:= Xl.Columns[3].ColumnWidth + 10: // Подавляем вывод диагностических сообщений Xl.DisplayAlerts : = False;
216 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office // Сохраняем документ Wb.SaveAs('d:\Custrep.xl s'); // Закрываем Excel и освобождаем ресурсы XI.Quit; X] := Unassigned; end; Как и в предыдущем случае, нам следует также объявить глобальные пере- менные для объектов Application, WorkBook и Worksheet: var Forml; TForml; // Переменные для обьектов Hord Application, Document, // Range и Table Wd. Doc, Rng, ТЫ: Variant: // Переменные для объектов Excel Application, DorkBook 11 и IkcrkSheet XI, Wb. Ws: Variant; Прокомментируем приведенный выше фрагмент кода. Во-первых, мы должны создать копию Microsoft Excel, сделать ее видимой и создать новую рабочую книгу: XI := CreateOleObject ('Excel.Application'); 11 Отображаем окно Microsoft Excel XI.Visible := True: // Создаем новую рабочую книгу XI.WorkBooks.Add: Wb := XL.WorkBooksfl]; Ws := Wb.WorkSheetsEU: Ws.Name := 'Customer list’; Затем нужно создать заголовок отчета и заголовки колонок будущей таблицы, добавляя текст в соответствующие ячейки и меняя характеристики шрифта этих ячеек: // Создаем заголовок отчета Ws.CellsEl.il := 'The list of customers'; Ws.Cells[l.l].Font.Bold := True: Ws.CellsEl.l].Font.Size ;= 16: Ws.Cel 1sE2.1] := 'CustomerlD'; Ws.CellsE2.2] := 'Customer Name'; Ws.Cel 1 sE2.3] := 'Country': for I := 1 to 3 do Ws.CellsE2.il.Font.Bold ;= True; Затем следует, перемещаясь по записям набора данных, добавить в документ строки, соответствующие этим записям: И Перемещаемся на первую запись набора данных ADODataSetl.First; for I := 1 to Rent do begin
Создание отчетов по базам данных с помощью приложений Office 217 И Добавляем значения полей текущей записи в новую строку Ws.Cel 1s[i+2,1] := ADODataSetl.FieldsfO].AsString: Ws.Cel 1s[i+2,2] := ADODataSetl.Fieldsfl].AsString; Ws.Cel 1s[i+2.3] := ADODataSetl.Fields[2].AsString: ADODataSetl.Next; end; Как и в предыдущем случае, нам следует изменить размер колонок па листе рабочей книги, чтобы корректно отобразить содержащиеся в них данные: // Изменяем ширину колонок XI.Columnsfl].ColumnWidth:= XI.Columnsfl].ColumnWidth + 5; XI .Columns[2].ColumnWidth: = XI.Columns[2].ColumnWidth + 30; XI.Columns[3].ColumnWidth:= XI.Columns[3].ColumnWidth + 10: Теперь нам нужно сохранить документ, подавив при этом вывод диагностиче- ских сообщений Excel. Как и Word, приложение Excel может быть запущено удаленно, и в этом случае пользователь также может пе иметь возможности взаимодействовать с диалоговыми окнами Excel: // Подавляем вывод диагностических сообщений XI .DisplayAlerts := False; // Сохраняем документ Wb.SaveAs('d:\Custrep.xls’); Наконец, нам следует закрыть Excel и освободить ресурсы: // Закрываем Excel и освобождаем ресурсы XI.Quit: XI := Unassigned: При выполнении этого приложения создается документ с отчетом по базе данных Northwind в формате Microsoft Excel (рис. 4.7). Итак, мы научились создавать простейшие табличные отчеты с помощью Microsoft Excel. Отметим, однако, что потребности пользователей обычно отнюдь не исчерпываются простейшими таблицами — нередко отчет должен содержать рисунки, внедренные объекты, деловую графику. Об этом мы поговорим в следую- щем разделе. Построение диаграмм в отчетах В последнем примере отчета мы создадим документ, содержащий деловую графику и опирающийся па вычислительные возможности Excel. Создаваемый отчет будет использовать набор данных, содержащийся в компоненте TAD0DataSet2 и представ- ляющий собой список компаний-клиентов и количество их заказов, сгруппиро- ванный по странам. Для каждой страны будет создан отдельный лист со сведе- ниями о клиентах из данной страны и графиком зависимости числа заказов от названия компании-клиента. На последнем листе будут собраны данные для всех стран и представлен график зависимости суммарного числа заказов для каждой страны, и эти сведения будут скопированы в документ Word.
218 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office £3 Microsoft Excel - Book.2 - a|.y| j®»* t* йе»» Ipurt Format look *v>x« TL .=121*1 й се ’• ”wial С35 В e: Caracas AAAa J : В У a. С,УУ . ... -..-’А- ...... AVAWVA A4SS л* tWWU'.W.'M .......W. «V-V<V.V. A.. AV .A'.-.v. .-‘-.AV. —. 1 The list of customers 2j CustomerlD Customer Name Country .... и '3 ALFKI Alfreds Futterkiste {Berlin Й 4 ANATR Ana Trujillo Emparedados у helados (Mexico D.F. d 5-, ANTON 'Antonio Moreno Taqueria (Mexico D.F. u. 6: AROUT > .round rhe Horn {London 7 BERGS (Berglunds snabbkop i Lulea 8 BLAUS Blauer See Delikatessen (Mannheim ?". 9 BLONP Rlondesddsl pere el fils i Strasbourg 10 BOLID Bolido Comidas preparadas (Madrid ii BONAP Bon app' (Marseille 12 BOTTM ;Bottom-Dollar Markets (Tsawassen . .. Г. 13 BSBEV P's Beverages iLondon 14 CACTU .Cactus Comidas para llevar (Buenos Ares 15 CENTC (Centro comercial Moctezuma {Mexico D.F. 16 CHOPS .Chop suey Chinese {Bern 17 COMMI Comercio Mineiro iSao Paulo 18 CONSH .Consolidated Holdings (London 19 DRACO Drachenblut Delikatessen Aachen 20 DUMON :Du monde entier (Nantes 21 EASTC Eastern Connection London 22 ERNSH Ernst Handel t Graz j wjj iki HKCustomer list /SheetZ Z Sheet3 7'~- »ir ( Ready Рис. 4.7. Отчет по базе данных, созданный путем автоматизации Microsoft Excel Создадим обработчик события, связанного со щелчком на кнопке Buttons, — в нем мы и реализуем описанную выше функциональность: procedure TForml.ButtonSCli ck(Sender: TObject); var I, J. Wpn: Integer; Country, Range_str: String; Sums. Countries: TStringList: begin Sums ;= TStringList.Create; Countries := TStringList.Create; // Перемещаемся на первую запись набора данных AD0DataSet2.First; // Запускаем Microsoft Excel Xl := CreateOleObject('Excel.Appl 1 cation'); // Делаем окно Excel видимым Xl.Visible := True; 11 Создаем одну рабочую книгу с одним листом XI.WorkBooks.Add(l);
Создание отчетов по базам данных с помощью приложений Office 219 Wb := XI.WorkBooks[1]; Ws := Wb.WorkSheetsEl]: repeat Country := ADODataSet2.FieldsE0].AsString; Ws.Name := Country; 11 Создаем заголовки листа и колонок Ws.Cells[l,l] := 'The number of orders for customers in ' + country; Ws.Cel 1 s[1.1].Font.Bol d:=True; Ws.Cel 1s[1.1].Font.Si ze:=16: Ws.CellsE2.1]:='CustomerID'; Ws.Cells[2.2]:='Customer Name'; Ws.Cells[2.3];='Quantity of orders'; for I := 1 to 3 do Ws.CellsE2,i].Font.Bold := True; I := 3; // Заполняем строки листа данными из записей repeat ws.Cel 1 s[ 1.1];=AD0DataSet2.Fi el ds E1].AsStri ng; ws.CellsEi.2]:=AD0DataSet2.Fields[2].AsString; ws.Cel 1s ti.3]:=AD0DataSet2.Fi el ds[3].AsStri ng; AD0DataSet2.Next; I := I + 1; until (not (Trim(ADODataSet2.Fields[OJ.AsString) = Country) or AD0DataSet2.Eof); // Вычисляем суммы Ws.CelIsti+1.2]:='Total for ’ + Country; Ws.CellsEi+1.3]:= '=SUM(C3..C + IntToStr(i-l) + ')'; // Сохраняем их для дальнейшего использования Sums.Add(IntToStr(Ws.CellsEi+1.3],Value)); Countries.Add(Country); // Изменяем размеры колонок XI .ColumnsElJ.ColumnWidth := XI .ColumnsEU .ColumnWidth + 5; XI.ColumnsЕ2].ColumnWidth : = XI.Columns[2].ColumnWidth + 30: XI .ColumnsЕЗ].ColumnWidth := XI.ColumnsE3].ColumnWidth + 10; // Выбираем данные для создания диаграммы Range_str := 'СЗ..C'+IntToStr(I - 1); Rng := Ws.range[range_str]; // Строим диаграмму Ch ;= Ws.Chartobjects.AdddO. I * 15 + 20. 400, I * 20 + 100); Ch.Chart.ChartW1zard(Rng, 2); // Устанавливаем свойства осей и заголовка диаграммы Ch.Chart.hasTitle := 1: Ch.Chart.hasLegend ;= False; Ch.Chart.ChartTitle.Text := 'The number of orders for customers in ' + country;
220 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Ch.Chart.Axesd) .hasTitle := True: Ch.Chart.Axes(l).AxisTitle.text := 'Company name'; Range_str : = 'A3..A' + IntToStrd - 1); Rng := Ws.Range[Range_str]: Ch.Chart.Axes(l).CategoryNames : = Rng: // Добавляем следующий лист Ws := Wb.Worksheets.Add: until AD0DataSet2.Eof: // Добавляем заголовок и названия колонок таблицы И для итогового листа Ws.Name := 'Total': Ws.Cells[l,1] := ' The number of orders for all countries ': Ws.CellsEl.lJ.Font.Bold := True: Ws.Cells[l.l],Font.Size : = 16: Ws.Cells[2.1] := 'Country'; Ws.Cells[2.2] := 'Number of orders'; for I:=l to 2 do Ws.Cells[2.i].Font.Bold : = True; // Заполняем строки сохраненными суммарными значениями for I := 0 to Countries.Count - 1 do begin Ws.Cel 1s[i+3.1] := Countries.StringsEiJ: Ws.Cel 1s[i+3.2] := StrToInt(Sums[i]); end; // Изменяем размер колонок XI .ColumnsEl].ColumnWidth -.= XI.columnsЕ2].ColumnWidth + 5; XI.ColumnsЕ2].ColumnWidth := XI.ColumnsE3],ColumnWidth + 10: // Выбираем диапазон для создания диаграммы Range_str := 'ВЗ..В' + IntToStrd + 2); Rng := Ws.RangeERange_str]; // Создаем диаграмму Ch := Ws.Chartobjects.AdddO, I * 15 + 30. 400. I * 15 + 60): Ch.Chart.Chartwizard(Rng. 2): 11 Устанавливаем свойства осей и заголовка диаграммы Ch.Chart.hasTitle := 1; Ch.Chart.hasLegend := False: Ch.Chart.ChartTitle.Text := 'The number of orders ': Ch.Chart.Axesd).hasTitle := True: Ch.Chart.Axesd).AxisTitle.text := 'Country': Range_str : = 'A3..A' + IntToStrd + 2); Rng := Ws.RangeERange_str]; Ch.Chart.Axesd).CategoryNames ;= Rng; //----------------------------------------------------- 11 Создаем экземпляр Microsoft Hord и делаем его видимым Wd := Create01e0bject('Word.Application'); Wd.Visible := True; // Создаем новый документ
Создание отчетов по базам данных с помощью приложений Office 221 Wd.Documents.Add; Doc ;= Wd.Documents.Item(l); 11 Создаем заголовок отчета Doc.Paragraphs.Add; Rng := Doc.Range(O); Rng.InsertBefore(' '): Rng.InsertBefore(' '); // Копируем график из рабочей книги Excel в буфер обмена Ch.Select; XI.Selection.Copy; // Помещаем график в документ Hord Rng := Doc.Ranged. 1): Rng.Seiect; Rng.Col lapse; Rng.PasteSpecial; // Копируем таблицу с итоговыми данными // с рабочего листа Excel Range_str ;= 'Al..В' + IntToStrd + 2); Rng := Ws.Range[Range_str]; Rng.Seiect; XI.Select!on.Copy: 11 И помещаем скопированную таблицу в документ Hord Rng := Doc.Range(25): Rng.Select; Rng.Col lapse; Rng.Paste: //--------------------------------------------------- // Подавляем вывод диагностических сообщений XI.DisplayAlerts := False; Wd.DisplayAlerts ;= False; // Сохраняем документы Wb.SaveAs('d:\Custrep.xls'): Doc.SaveAs('d:\Custrep.doc'); 11 Закрываем приложения Office и освобождаем ресурсы XI.Quit; XI := Unassigned: Wd.Quit: Wd := Unassigned; Countries.Free: Sums.Free; end: Прокомментируем приведенный выше фрагмент кода. Во-первых, мы должны создать объекты TStringList для храпения итоговых значений. Sums := TStringList.Create; Countries := TStringList.Create;
222 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Затем нам нужно создать копию Microsoft Excel, сделать ее видимой и соз- дать новую рабочую книгу: И Запускаем Microsoft Excel XI := CreatedeObject('Excel.Application'); // Делаем окно Excel видимым XI.Visible := True: Следующий шаг — это перебор списка стран в наборе данных: AD00ataSet2.First: repeat until AD0DataSet2.Eof; В теле этого цикла мы создаем заголовки отчета и колонок, подобно тому, как это было сделано в предыдущем примере: Country := ADODataSet2.FieldsfO].AsString; Ws.Name := Country: // Создаем заголовки листа и колонок Ws.Cellsfl.l] ;= 'The number of orders for customers in ' + country; Ws.Cellsfl.l],Font.Bold := True: Ws.Cellsfl.l].Font.Size := 16: Ws.Cellsf2,l] := 'CustomerlD': Ws.Cellsf2,2] := 'Customer Name': Ws.Cellsf2,3] := 'Quantity of orders’; for i:=1 to 3 do Ws.Cel 1sf2.i].Font.Bold:=True; i := 3; Далее нам следует заполнить ячейки рабочего листа значениями полей из тех записей, в которых значение поля Country одно и то же (вспомним, что записи в пашем наборе данных сгруппированы по данному полю): // Заполняем строки листа данными из записей repeat ws.Cellsfi.l]:=ADODataSet2.Fieldsfl],AsString; ws.Cel 1s fi.2]:=AD0DataSet2.Fi eldsf2].AsStri ng; ws.Cel 1s f i.3]:=AD0DataSet2.Fi el ds Г 3].AsString: AD0DataSet2.Next; I := I + 1; until (not (Trim(ADODataSet2.FieldsfO],AsString)=Country) or AD0DataSet2.Eof); Далее нам следует вычислить суммарные значения (это мы сделаем с помо- щью Excel) и поместить их в объекты TStringList: // Вычисляем суммы Ws.Cells[1+1.2] := 'Total for ' + Country;
Создание отчетов по базам данных с помощью приложений Office 223 Ws.Cells[i+1.3] := =SUM(C3..C’ + IntToStrd - 1) + // Сохраняем их для дальнейшего использования Sums.Add(IntToStr(ws.Cells[i+l.3].value)); Countries.Add(country); Далее можно изменить ширину колонок рабочего листа. Чтобы создать диаграмму, нужно выбрать диапазон ячеек и на его основе соз- дать внедренный объект Chart: // Строим диаграмму Ch := Ws.Chartobjects.AdddO. I * 15 + 20, 400, I * 20 + 100): Ch.Chart.ChartWizard(Rng, 2): Изменим параметры диаграммы (легенду, оси, надписи): // Устанавливаем свойства осей и заголовка диаграммы Ch.Chart.hasTitle := 1; Ch.Chart.hasLegend := False: Ch.Chart.ChartTitle.Text := 'The number of orders for customers in ' + Country; Ch.Chart.Axes(l).hasTitle := True: Ch.Chart.Axes(l).AxisTitle.text := 'Company name': Нам следует также выбрать колонку, которая содержит значения категорий (в данном случае — имена компаний): Range_str : = 'АЗ..А' + IntToStr(I - 1): Rng := Ws.Range[Range_str]: Ch.Chart.Axes(1).CategoryNames : = Rng; Наконец, нужно создать пустой рабочий лист для следующей страны: // Добавляем следующий лист Ws := Wb.Worksheets.Add; Далее нам следует заполнить последний рабочий лист суммарными значе- ниями. После создания заголовков отчета и колонок нужно заполнить ячейки итоговыми значениями, сохраненными ранее в объектах TStringList: // Заполняем строки сохраненными суммарными значениями for I : =0 to Countries.Count - 1 do begin Ws.Cells[1+3.1] := Countries.Strings[i]; Ws.Cel 1s[1+3,2] := StrToInt(Sums[i]); end; Далее мы выбираем диапазон ячеек для построения итогового графика и ме- няем параметры этого графика: // Выбираем диапазон для создания диаграммы Range_str : = 'ВЗ..В' + IntToStrd + 2); Rng := Ws.Range[Range_str]; // Создаем диаграмму
224 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Ch := Ws.Chartobjects. AdddO, I * 15 + 30. 400. I * 15 + 60); Ch.Chart.Chartwizard(Rng. 2); 11 Устанавливаем свойства осей и заголовка диаграммы Ch.Chart.hasTitle := 1; Ch.Chart.hasLegend := False; Ch.Chart.ChartTitle.Text := 'The number of orders ’; Ch.Chart.Axes(l).hasTitle := True; Ch.Chart.Axes(l).AxisTitle.text := 'Country'; Range_str : = 'A3..A' + IntToStr(i + 2); Rng := Ws.Range[Range_str]; Ch.Chart.Axes(l).CategoryNames ;= Rng; Далее следует создать экземпляр Word, сделать его видимым, создать в нем новый документ и добавить в него несколько пробелов, которые затем заменим диаграммой, скопированной из Excel: И Создаем экземпляр Microsoft Word и делаем его видимым Wd := CreateOleObject('Word.Application'); Wd.Visible ;= True: 11 Создаем новый документ Wd.Documents.Add; Doc ;= Wd.Documents.Item(l); 11 Создаем заголовок отчета Doc.Paragraphs.Add; Rng := Doc.Range(O): Rng.InsertBeforeC '); Rng.InsertBefore(' '): // Копируем график из рабочей книги Excel в буфер обмена Ch.Seiect: Xl.Seiection.Copy: // Помещаем график в документ Word Rng := Doc.Ranged. 1); Rng.Select: Rng.Col lapse; Rng.PasteSpecial; Осталось скопировать в Word таблицу с данными, па основании которых по- строена диаграмма: // Копируем таблицу с суммарными данными с рабочего листа Excel Range_str : = 'Al..В' + IntToStrd + 2); Rng ;= Ws.Range[Range_str]; Rng.Seiect; Xl.Select!on.Copy; // И помещаем скопированную таблицу в документ Word Rng := Doc.Range(25); Rng.Select; Rng.Col 1 apse; Rng.Paste:
Создание отчетов по базам данных с помощью приложений Office 225 В конце фрагмента кода мы подавляем вывод диагностических сообщений, сохраняем созданные документы, закрываем приложения Office и освобождаем ресурсы. Результатами работы приложения будет рабочая книга Excel с числом лис- тов, равным числу стран в исходном наборе данных, и с данными о заказчиках из выбранной страны и диаграммами па каждом листе (рис. 4.8) Кроме того, мы получим документ Word с итоговыми данными и диаграммой, построенной с по- мощью Excel па основе этих данных (рис. 4.9). Рис. 4.8. Рабочая книга Excel, содержащая отчет с внедренными диаграммами Таким образом, путем автоматизации приложений Microsoft Office мы можем создавать сложные отчеты, содержащие таблицы, диаграммы, вычисляемые зпа чепия, равно как и использовать для этой цели другие возможности Microsoft Office (например, сервисы построения сводных таблиц).
226 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Рис. 4.9. Документ Word с внедренной диаграммой Excel Следует отметить, что отчеты подобного типа можно создать, используя не- посредственно объект Recordset, без применения компонентов ADOExpress или иных наследников класса TDataSet. В этом случае можно обращаться к свойству Fields объекта Recordset. Подобный код может выглядеть, например, так: 01eCheck(CoCreateInstance(CLASS_Recordset. nil. CLSCTX_ALL. IID__Recordset, rec)); // Определяем источник данных dsn := 'Provider=SQLOLEDB.l:Integrated Security=SSPI;' + 'Persist Security Info=False:Initial Catalog=Northwind:' + 'Data Source=MAINDESK;Locale Identifier=1049; ' + 'Connect Timeout=15: Use Procedure for Prepare=l:' + 'Auto Translate=True:Packet Size=4096; ' + 'Workstation ID=CHILD'; // Открываем набор данных (Recordset)
Применение коллекций 227 Rec.Open('select Country, CompanyName from customers'. dsn.adOpenForwardOnly, adLockReadOnly. adCmdllnspecified): I := 0: repeat Ws.Cells[i+2.1] := Rec.FieldsfOL Value: Ws.Cells[i+2.2] : = Rec.Fieldsfl],Value; Ws.Cells[i+2.3] := Rec.Fields[2].Value: Rec.Moved. EmptyParam): I := I + 1: until Rec.Eof: Помимо этого при создании отчетов можно воспользоваться средствами доступа к данным самих приложений Microsoft Office, о которых мы упоминали в раз- деле, посвященном созданию контроллера автоматизации Microsoft Excel. Ниже приведен пример применения коллекции QueryTables объекта Worksheet Microsoft Excel для генерации отчета по данным таблицы Microsoft SQL Server: Wb := Арр.WorkBooks.Add; Ws := Wb.ActiveSheet; Qt := Ws.QueryTables.Add('ODBC;DRIVER=SQL Server;'+ ’SERVER=MAINDESK:UID=Administrator;' + 'APP=Microsoft Office XP:WSID=MAINDESK:'+ 'DATABASE=Northwind;Trusted_Connection=Ves', Ws.Range['B2:B2']): Qt.CommandText := 'SELECT * FROM Orders': Qt.FieldNames := True; QT.AdjustColumnWidth : = True: Qt.Refresh: Продолжая тему позднего связывания, рассмотрим еще один пример, иллюстри- рующий применение коллекций, общих для всех приложений Microsoft Office. Применение коллекций В качестве примера использования коллекций объектов Microsoft Office рассмот- рим программу, извлекающую из приложений Office значки, характерные для кнопок и меню (рис. 4.10). В силу того, что, как мы убедились выше, приложения Microsoft Office имеют унифицированную структуру объектов, программа получилась очень простой, состоящей из единственной процедуры извлечения значков. В этой главе мы рас- смотрим лишь те аспекты ее работы, которые имеют отношение к автоматизации. В начале кода нашей программы объявляем идентификаторы используемых OLE-серверов: const ObjWord = 'Word.Appl1cation': ObjExcel = 'Excel.Application': ObjAccess = 'Access.Application';
228 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office ObjPowerPoint = 'PowerPoint.Application'; ObjFrontPage = 'FrontPage.Application’; ObjVisio = 'Visio.InvisibleApp': ObjVStudio = 'VSAIDE.DTE'; Рис. 4.10. Приложение для извлечения значков из приложений Office При старте мы проверяем, какие из этих серверов установлены па данном компьютере, и делаем доступными соответствующие кнопки: procedure TForml.FormCreatefSender: TObject): begin with TRegistry.Create do try RootKey := HKEY_CLASSES_ROOT: bWord.Enabled := KeyExists(ObjWord): bExcel.Enabled := KeyExists(ObjExcel): bAccess.Enabled := KeyExists(ObjAccess): bPowerPoint.Enabled := KeyExists(ObjPowerPoint): bFrontPage.Enabled := KeyExists(ObjFrontPage); bVisio.Enabled := KeyExists(ObjVisio): bVStudio.Enabled := KeyExists(ObjVStudio); finally Free: end; LB.Caption := Formate'Xd images'. [1vList.Items.Count]): end; При щелчке па кнопке создается OLE-сервер, и ссылка па него передается в процедуру Dolt:
Применение коллекций 229 procedure TForml.WordClick(Sender: TObject): begin Dolt(CreateOleObject(ObjWord)): end; Эта процедура организует цикл по коллекции CommandBars, содержащей панели инструментов с кнопками и меню приложения. procedure TForml.DoIt(App: Variant): var I. J: Integer: CB: CommandBar; begi n В := TBitmap.Create: try PB.Max := App.CommandBars.Count: for I := 1 to App.CommandBars.Count do begin PB.Position := I: PB.Update; CB := IDispatch(App.CommandBars[I]) as CommandBar; FBarName : = CB.Name: SB.SimpleText := FBarName; for J := 1 to CB.Controls.Count do CheckControl(CB.ControlsCJJ): end; App.Quit: App := Unassigned: SB.SimpleText := ’Done’: finally B.Free; end; end: Основная работа, связанная с поиском значков, реализуется в методе Check- Control, который рекурсивно обходит элементы интерфейса, имеющиеся па ком- поненте CommandBar, а также вложенные элементы интерфейса: procedure TForml.CheckControl(Control: CommandBarControl); var Button: CommandBa rButton; Popup: CommandBarPopup: Name: String; I: Integer: begin try // далее следует обход элементов интерфейса И компонента CorrmandBar
230 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office Проверяем, верно ли то, что переданный элемент — CommandBarButton. Если это не так, то будет сгенерировано исключение. В противном случае — мы получаем ссылку па кнопку, означающую, что у кнопки есть значок: Button ;= Control as CommandBarButton; Name := Control.Caption; К сожалению, Office не предоставляет средств для доступа к значку па кнопке, поэтому единственный способ получить его — скопировать изображение в буфер обмена: Button.CopyFace: Удаляем из имени кнопки символ & и концевую точку: while Pos(. Name) > 0 do System.Delete!Name. Pos('&’, Name). 1): while Name[Length(Name)] = do System.Delete(Name. Length(Name). 1): Далее вызываем метод AddToBmpList, который сохраняет скопированное в буфер обмена изображение в компоненте HmageList и добавляет строку с именем кнопки в компонент TListView. Кроме того, производится проверка на наличие дублирую- щихся изображений (по контрольной сумме) и при необходимости генерируется уникальное имя: AddToBmpList(Name); except Если произошло исключение, это означает, что выбранный элемент — не CommandBarButton. Это может быть элемент CommandBarPopup, содержащий вложен- ное меню. В этом случае надо осуществить проход по его вложенным элементам: try Popup := Control as CommandBarPopup; Name ;= Popup.Caption; while Pos('&', Name) > 0 do System.Delete(Name. Pos('&’. Name). 1): SB.SimpleText := FBarName + ' - ’ + Name: for I := 1 to Popup.Controls.Count do CheckControl(Popup.Controlsfl]) except end; // завершаем работу метода CheckControl end: end: В результате работы этой программы вы получаете в свое распоряжение свыше тысячи значков. Полный исходный текст программы приведен па прилагаемом компакт-диске.
Применение раннего связывания 231 Применение раннего связывания Поскольку все приложения Microsoft Office обладают библиотеками типов, по- мимо рассмотренного выше позднего связывания, можно применять и раннее связывание, что во многих случаях позволяет получать приложения, отличающиеся более высокой производительностью по сравнению с приложениями, основанными на позднем связывании. Однако в этом случае, как было сказано выше, к коду приложения предъявляются более серьезные требования. Поскольку в случае раннего связывания производится вызов методов классов Delphi, сгенерирован- ных на основании библиотеки типов приложения Office, все параметры вызывае- мых методов должны соответствовать описаниям методов в интерфейсном модуле. Иными словами, мы не имеем права опускать параметры, если пас устраивает их значение по умолчанию. Еще одно преимущество раннего связывания — возможность достаточно про- сто реализовать обработку событий сервера. Особенно просто это сделать, сгене- рировав компоненты, соответствующие объектам Office. Отметим, что компоненты для последних версий Microsoft Office (Office 97, 2000 и ХР) входят в комплект поставки Delphi 7 — один из них следует выбрать при установке. Как показывает практика, в общем случае компоненты должны соответствовать конкретной вер- сии Office и при ее замене следует удалить соответствующую страницу палитры компонентов и соответствующие DCU-файлы, а затем сгенерировать новые, вы- брав команду Project ► Import Type Library и установив флажок Generate Component Wrapper в открывшемся диалоговом окне Import Type Library. Решив вопрос соответствия компонентов страницы Servers установленной версии Office, разработаем простейшее приложение, обрабатывающее события сервера. С этой целью создадим новый проект, поместим па его форму кнопку и компонент TExcelApplication. Создадим обработчик события, связанный со щелчком па кнопке, — пусть при этом запускается приложение Excel и его глав- ное окно становится видимым для пользователя: const LCID = 0: procedure TForml.ButtonlClick(Sender: TObject): begin ExcelAppl1cati onl.Connect: ExcelApplicationl.Vi si ble[LCID]:=True: end: Обработаем два события компонента ExcelApplicationl — OnWindowResize и OnNewWorkbook. Так выглядит версия обработчиков этих событий в случае при- менения компонентов Servers, сгенерированных в предположении, что серве- рами автоматизации являются приложения семейства Microsoft Office 2000: procedure TForml.ExcelApplicationlWindowResize(Sender: TObject: var Wb, Wn: 01 eVari ant):
232 Глава 4. Создание контроллеров автоматизации приложений Microsoft Office begin ShowMessaget'The Excel window was resized’); end; procedure TForml.ExcelApplicationlNewWorkbook(Sender: TObject; var Wb: OleVariant): begin ShowMessaget'A new workbook was created'); end; А так выглядит версия обработчиков этих событий в случае применения ком- понентов Servers, сгенерированных в предположении, что серверами автоматиза- ции являются приложения семейства Microsoft Office ХР: procedure TForml.ExcelApplicationlWindowResize(ASender: TObject; const Wb; _Workbook; const Wn: Window); begi n ShowMessaget'A new workbook was created'); end: procedure TForml.ExcelApplicationlWindowResize(ASender: TObject; const Wb: -Workbook; const Wn; Window); begin ShowMessaget'The Excel window was resized'); end; Запустив приложение, мы можем убедиться, что при создании повой рабочей книги и при изменении размеров окна с рабочей книгой па экране появляются соответствующие сообщения. Итак, в данном разделе мы изучили возможности применения раннего связы- вания для обработки событий, возникающих в приложениях Microsoft Office. Заключение В данной главе мы рассмотрели автоматизацию приложений Microsoft Office, а именно Microsoft Word, Microsoft Excel, Microsoft PowerPoint и Microsoft Out- look, наиболее часто используемых в качестве контроллеров автОхматизации. Мы узнали, что приложения Microsoft Office предоставляют контроллерам автома- тизации доступ к своей функциональности с помощью своей объектной модели, представляющей собой иерархию объектов, а те, в свою очередь, могут предостав- лять доступ к другим объектам посредством коллекций. Мы рассмотрели основные задачи, связанные с автоматизацией Microsoft Word, а именно создание, открытие, сохранение, печать и закрытие документов, вставку текста и объектов в документ, форматирование текста, перемещение курсора по тексту, создание таблиц, обращение к свойствам документа. Мы обсудили основные задачи, связанные с автоматизацией Microsoft Excel, такие как создание, открытие, сохранение, печать и закрытие рабочих книг, обра-
Заключение 233 щение к их листам и ячейкам, создание диаграмм, доступ к реляционным дан- ным и OLAP-кубам. Мы изучили автоматизацию Microsoft PowerPoint, в частности создание, от- крытие, сохранение, печать и закрытие презентаций Microsoft PowerPoint, их оформление, манипуляцию отдельными слайдами, управление демонстрацией слайдов. Мы рассмотрели основные задачи, связанные с автоматизацией Microsoft Outlook, такие как открытие и создание нанок, манипуляция их элементами, а так- же сообщениями электронной почты, контактами, заметками и задачами. Мы рассмотрели пример, использующий коллекции объектов Microsoft Office. Мы обсудили применение раннего связывания и компонентов страницы Servers палитры компонентов при автоматизации приложений Microsoft Office, что во многих случаях позволяет повысить производительность контроллера автоматиза- ции. Мы убедились, что, применяя раннее связывание и генерацию компонентов для интерфейса к серверам Office, мы получаем возможность достаточно просто реализовать обработку событий этих серверов. Отметим, однако, что обращение к объектам Office, равно как и к ряду других серверов автоматизации, может производиться не только путем автоматизации. В следующей главе мы расскажем, как можно обращаться к методам этих же сер- веров, когда они применяются для отображения OLE-документов па формах.
ГЛАВА 5 Использование OLE-документов в приложениях Настоящая глава посвящена работе с OLE-документами. OLE-документы — это, по существу, внедренные или связанные объекты, созданные приложениями, вы- ступающими в качестве COM-серверов. Правила создания контейнеров для та- ких объектов (называемых иногда OLE-контейнерами) описывает соответствую- щая спецификация СОМ, и одно из основных требований этой спецификации — так называемая активация по месту (in-place activation), то есть реализация воз- можности редактирования объекта, находящегося в OLE-контейнере, за счет ав- томатического запуска для этой цели приложения-сервера. Для создания и использования OLE-документов в приложениях, разрабаты- ваемых в Delphi, предназначен компонент TCI eContainer, находящийся на странице System палитры компонентов. Этот компонент инкапсулирует все интерфейсы, необходимые для создания клиентов OLE-докумептов. Создание и отображение OLE-документов в формах Компонент Т01 eContainer позволяет поместить OLE-документ на поверхность формы. Наиболее часто используемыми свойствами этого компонента являются AutoActivate, определяющее, каким образом активизируется OLE-докумепт, State, определяющее состояние OLE-контейнера, и 01 eCi assName, определяющее имя класса (CLSID) OLE-объекта, содержащегося в контейнере. Наиболее часто ис- пользуются следующие методы этого компонента: Ж InsertObjectDialog — выводит стандартное диалоговое окно Object для выбора типа документа или загрузки его из файла; ж CreateObject — создает OLE-объект; И CreateCbjectFromFile — создает OLE-объект па основе существующего файла, содержащего OLE-докумепт, и помещает его в OLE-коптейпер; • DestroyObject — уничтожает объект, содержащийся в OLE-коптейпере. Создадим простейшее приложение, иллюстрирующее использование компонента Т01 eContainer. С этой целью поместим на форму компонент TPanel со свойством Align, равным al Cl i ent, па пего — компонент TOleContainer и главное меню (можно создать в нем пункты New object и Exit). Панель и меню нужны для отображения панелей инструментов и меню OLE-серверов, обслуживающих отображаемые
Создание и отображение OLE-документов в формах 235 в компоненте Т01eContainer объекты. Если па форме, содержащей компонент Т01 eContainer, имеется меню, то меню сервера будет присоединено к меню прило- жения согласно правилам слияния меню, принятым в Windows. Если компонент Т01 eContainer помещен на компонент TPanel, последний будет отображать панель инструментов сервера (рис. 5.1). Рис. 5.1. Форма с компонентом TOIeContainer Создадим обработчик события, связанный с выбором пункта меню New Object: procedure TForml.NewlClick(Sender: TObject): begin 01eContai nerl.InsertObjectDi alog: end: Запустив приложение и щелкнув на кнопке, получим диалоговое окно Insert Object (рис. 5.2). Рис. 5.2. Диалоговое окно вставки объекта В списке, представленном в этом диалоговом окне, перечислены все серверы OLE-документов, зарегистрированные на данном компьютере. Можно выбрать
236 Глава 5. Использование OLE-документов в приложениях один из них (например, Microsoft Excel Worksheet). Теперь после двойного щелчка на компоненте Т01 eContainer компонент TPanel будет содержать панель инструментов Microsoft Excel и главное меню этого приложения, а сам OLE-koh- тейпер — новую рабочую книгу Excel (рис. 5.3). Рис. 5.3. Активный объект в OLE-контейнере Если тип объекта, отображаемого в OLE-коптейпере, известен заранее, можно использовать метод CreateObject компонента TOleContaiпег: procedure TForml.NewExcelworksheetlClick(Sender: TObject); begin 01 eContainert.CreateObject!'Excel.Sheet'. False); end; Второй параметр этого метода указывает, отображать ли в виде значка объект внутри OLE-контейнера. ВНИМАНИЕ --------------------------------------------------------------------- Хотя что в диалоговом окне Insert Object содержатся только имена серверов OLE-доку- ментов, отображать в подобных компонентах можно в принципе любые СОМ-серве- ры, обладающие пользовательским интерфейсом, в частности многие из элементов управления ActiveX. В случае если необходимо отобразить в OLE-коптсйпсре поль- зовательский интерфейс СОМ-сервера, пе являющегося сервером OLE-докумеп- тов, следует указать его идентификатор CLSID в качестве первого параметра метода CreateObject. Модифицируем приложение, добавив еще несколько пунктов меню (рис. 5.4). Создадим соответствующие обработчики событий: procedure TForml.ShowPropertieslClick(Sender: TObject); begin if 01 eCOntainerl.01 eObject Interface <> nil then 01eContai nerl.ObjectProperti esDi alog
Создание и отображение OLE-документов в формах 237 else ShowMessage ('01 eContainer is empty'); end; procedure TForml.PastespeciallClick(Sender: TObject); begin 01eContai nerl.PasteSpeci al Di alog: end: Рис. 5.4. Меню приложения, использующего компонент TOIeContainer Скомпилируем приложение и па этапе выполнения добавим какой-нибудь объ- ект в OLE-коптейпер. Выбрав команду Action ► Show Properties нашего приложения, получим стандартное диалоговое окно с описанием свойств OLE-объекта (рис. 5.5). Рис. 5.5. Диалоговое окно с описанием свойств объекта
238 Глава 5. Использование OLE-документов в приложениях Поместив какие-либо данные в буфер обмена, выберем команду Action ► PasteSpecial нашего приложения и получим диалоговое окно специальной вставки объекта из буфера обмена Paste Special (рис. 5.6). Рис. 5.6. Диалоговое окно специальной вставки Выбрав тин вставляемого объекта в предложенном списке, мы можем помес- тить его в OLE-контейнер (рис. 5.7). Рис. 5.7. Результат вставки объекта из буфера обмена Управление объектом внутри OLE-контейнера Как управлять объектом, помещенным в компонент TOIeContainer? После того как объект помещен в OLE-контейнер, с ним можно выполнить некоторый ограниченный набор действий с помощью свойства Objectverbs. Метод
Управление объектом внутри OLE-контейнера 239 DoVerb позволяет выполнить одно из этих действий, ссылаясь па его порядковый номер в списке, а целочисленное свойство PrirnaryVerb содержит номер действия из этого списка, выполняющегося при активизации OLE-объекта. Как правило, этих действий два — одно для отображения объекта, другое — для инициирования процесса его редактирования с помощью сервера (обычно в отдельном окне). В качестве иллюстрации применения этого свойства на панель с компонен- том TOleContaiпег поместим компонент TComboBox и установим его свойство Enabled равным False (рис. 5.8). Рис. 5.8. Пример использования свойств компонента TOIeContainer Изменим обработчики событий: procedure TForml.NewlClick(Sender: TObject); begin 01eContai nerl.InsertObjectDi alog: ComboBoxl.Items := 01eContainerl.Objectverbs: ComboBoxl.Enabled := True; end; procedure TForml.NewExcelworksheetlClick(Sender: TObject): begin 01 eConta i ner1.CreateObject('Excel.Sheet'. False); ComboBoxl.Items := 01eContainerl.Objectverbs: ComboBoxl.Enabled := True: end: procedure TForml.ComboBoxlChange(Sender: TObject): begin if 01eContainerl.State <> osEmpty then 01eContai nerl.DoVerb(ComboBoxl.Itemindex); end:
240 Глава 5. Использование OLE-документов в приложениях Теперь после помещения объекта в OLE-коптейпер мы можем выбрать одно из действий, доступных в комбинированном списке. Например, создадим в OLE- коптейпере растровый рисунок (объект Paintbrush Picture) и выберем действие Open. Это приведет к открытию приложения-сервера в отдельном окне, при этом в процессе редактирования изображения будет происходить синхронное измене- ние объекта в OLE-коптейпере (рис. 5.9). Рис. 5.9, Пример использования свойства Objectverbs компонента TOIeContainer Выбор же действия Edit приведет к редактированию объекта непосредственно в самом OLE-коптейпере. Отметим, что если объект в OLE-коптейпере экспонирует какие-либо свойства и методы, доступ к ним может быть осуществлен с помощью свойства 01 eObject компонента TOIeContainer. Используя это свойство, мы можем реализовать дейст- вия, доступные контроллерам автоматизации того же самого сервера, за исклю- чением, естественно, его удаленного запуска. Для иллюстрации этих возможностей поместим в OLE-коптейпер лист Micro- soft Excel и создадим в нем сводную таблицу, для чего воспользуемся уже знако- мой нам по примерам из предыдущей главы базой данных Northwind, входящей в комплект поставки Microsoft Office Professional. Добавим к меню Action нашего приложения еще один пункт — Create Pivot Table — и создадим обработчик собы- тия, связанного с выбором этого пункта меню: var Forml: TForml; WB, PC. PT: Variant:
Управление объектом внутри OLE-контейнера 241 const // константы Excel xl External = $00000002; xlCmdSql = $00000002; xlColumnField = $00000002 xlDataField = $00000004; xlPageField = $00000003; xlRowField = $00000001: procedure TForml.CreatePivotTablelClick(Sender: TObject); begi n 01eContainerl.CreateObject!'Excel.Sheet’. False); WB := OleContainerl.OleObject; // Создаем кэш дня хранения данных PC := WB.PivotCaches.Add(xlExternal); // Выбираем источник данных и текст запроса PC.Connection := '0LEDB;Provider=Microsoft.Jet.0LEDB.4.0;’ + 'Data Source=C;\data\Northwind.mdb'; PC. CommandType := xlCmdSql; PC.CommandText ;= 'SELECT Country, City,' + ' ProductName, Salesperson, ExtendedPrice FROM Invoices '; // Создаем сводную таблицу PC.CreatePi votTable (WB.Worksheets[1].Cel1s[l,1], 'PivotTablel'): PT := WB.Worksheets[l],PivotTables!'PivotTablel'); // Указываем расположение осей и суммируемые данные PT.PivotFields!'Country').Orientation := xlRowField; PT.PivotFields('Country').Position := 1; PT.PivotFields!'City').Orientation := xlRowField: PT.PivotFields!'City').Position := 2: PT.PivotFields('ProductName').Orientation := xlPageField; PT.PivotFields('Salesperson').Orientation := xlColumnField: PT.PivotFields('ExtendedPrice').Orientation := xlDataField; WB.Worksheets[l].Columns[2],ColumnWidth ;= 15; WB.Worksheets[l].Columns[lJ.ColumnWidth := 20; end: В приведенном выше фрагменте кода мы создаем кэш для хранения данных, требующихся для построения сводной таблицы, затем описываем его свойства, такие как его имя, свойства источника данных (OLE DB Connection String), текст SQL-запроса, затем создаем сводную таблицу и, наконец, размещаем поля набора данных, являющегося результатом запроса, на ее осях. Результат выполнения указанного выше фрагмента кода представлен па рис. 5.10. При необходимости можно выполнить с данными объекта, содержащегося в OLE-контейнере, любые манипуляции, доступные методам соответствующего сервера OLE-документов. Можно также минимизировать возможное вмешатель- ство пользователя в этот процесс, запретив активацию объекта путем установки значения свойства AutoActivate равным aaManual, а значения свойства AutoVerbMenu
242 Глава 5. Использование OLE-документов в приложениях равным False. В этом случае у пользователя не будет возможности вносить изме- нения в объект, если только он заранее не запустил соответствующий сервер OLE-документов, пользовательский интерфейс которого можно применить для редактирования данных этого объекта. Рис. 5.10. Результат обращения к методам объекта в OLE-контейнере Хранение OLE-объектов в базах данных Задача храпения OLE-объектов в базах данных встречается па практике довольно часто. Некоторые СУБД (такие как Microsoft Access, Microsoft SQL Server, Micro- soft Desktop Engine) обладают специально предназначенными для этого типами полей. Отметим, однако, что далеко не все СУБД обладают такими типами данных. Тем пе менее хранить OLE-объекты можно практически в любой базе данных, по- скольку для этой цели могут быть использованы любые BLOB- и МЕМО-поля баз данных. Основная идея приложения, сохраняющего содержимое OLE-контейнера в базе данных, заключается в том, что объект, находящийся в компоненте TOleContaiпег, записывается в файл или память, а затем помещается в BLOB-поле. Ниже мы рассмотрим несколько способов реализации такого приложения, от самого при- митивного до наиболее корректного.
Хранение OLE-объектов в базах данных 243 Использование временного файла Создадим простейший пример такого приложения. С этой целью поместим па форму вновь созданного приложения компонент TPanel со свойством Align, равным alClient, а па пего — компоненты TOIeContainer, TDBNavigator, ТТаЫе, TDataSource и три кнопки с надписями New Object, Save и Show. Создадим копию таблицы biolife.db из набора демонстрационных примеров, свяжем с ней компонент ТТаЫе и создадим для него набор объектов TFields. Свя- жем компонент TDataSource с компонентом ТТаЫе, а компонент TDBNavigator — с компонентом TDataSource. Изменим обработчики событий: procedure TForml.Button2Click(Sender: TObject): begin if OleContainerl.OleObjectlnterface <> nil then begin 01eContainerl.SaveToFile('aaa.dat'): Tablet.Edit; Tabl elGraphi c.LoadFromFi1e('aaa.dat'): end: end; procedure TForml.Button3Click(Sender: TObject): begin TablelGraphi c.SaveToFi1e('aaa.dat’); 01eContai nerl.LoadFromFi1e(’aaa.dat’): end: Теперь, если OLE-контейнер не пуст, при щелчке на кнопке Save его содер- жимое сохраняется в BLOB-поле таблицы, а при щелчке на кнопке Show OLE- объект из BLOB-поля отображается в OLE-коптейпере (рис. 5.11). Рис. 5.11. Неактивный OLE-объект, изъятый из BLOB-поля щелчком на кнопке Show
244 Глава 5. Использование OLE-документов в приложениях Использование памяти и методов-наследников класса TDataSet Предыдущий пример имеет массу недостатков. Главным из них является тот факт, что пользователь должен вручную инициировать загрузку содержимого BLOB- поля в компонент TOIeContainer и сохранение содержимого TOIeContainer в таблице. Такое поведение приложения противоречит традиционному принципу создания пользовательских интерфейсов для форм просмотра и редактирования данных, гласящему, что данные в интерфейсных элементах при перемещении по записям, содержащимся в доставленном па рабочую станцию наборе данных, должны об- новляться автоматически и сохраняться автоматически. Иное поведение прило- жения попросту дезориентирует пользователя, привыкшего к стереотипу в пове- дении подобных приложений. Если преодолеть первый недостаток данного приложения, добившись автома- тического обновления данных в OLE-контейнере и сохранения их в таблице, тут же проявится его второй недостаток, заключающийся в постоянном сохранении и загрузке данных с диска. Дисковые операции вполне допустимы при ручных манипуляциях с OLE-контейнером, по при автоматическом обновлении и сохране- нии они могут привести к заметному снижению производительности приложения. Реализуя принцип автоматического обновления и сохранения данных при вы- полнении навигационных методов компонентов TDataSet, следует учитывать, что компонент TOIeContainer не может быть связан с источником данных. Поэтому в простейшем случае необходимо использовать методы BeforeScrol 1 и AfterScrol 1 компонента-наследника класса TDataSet. В обработчике события BeforeScrol 1 следует проверить, было ли изменено содержимое OLE-контейнера. Это достигается путем проверки свойства Modified компонента TOIeContainer. Если в этом компоненте были произведены какие-либо изменения, то создается поток данных в памяти, куда первоначально заносится некоторая последовательность байтов (назовем ее условно «цифровой подписью»; стоит, однако, отдавать себе отчет в том, что к настоящей цифровой подписи эта последовательность байтов отношения не имеет). Подпись будет в дальнейшем нужна при чтении данных: анализируя ее, можно определить, находятся ли данные для компонента TOIeContainer в текущей записи. Затем используется метод SaveToStream компонента TOIeContainer: const Signature: Integer = -525465623: procedure TForml.TablelBeforeScrolК DataSet: TDataSet): var Stream: TMemoryStream: begin if 01 eContainerl.Modified then begin Stream : = nil; try Stream := TMemoryStream.Create; Stream.Write(Signature. SizeOf(Signature)): if Assigned(OleContainerl.OleObjectlnterface) then
Хранение OLE-объектов в базах данных 245 01eConta i nerl.SaveToSt ream(St ream); Stream.Seek(O. soFromBeginning); try Tablet.Edit; TBLOBField(TaNel.FieldByName('Graphic')). LoadFromStream(Stream); Tablet.Post; OleContainerl.Modified ;= False; except Tablet.Cancel; end; finally if Assigned(Stream) then Stream.Free; end; end; end; Прежде всего обработчик события AfterScroll очищает текущее содержимое OLE-контейнера. После этого создается поток данных в памяти, куда и перено- сится содержимое BLOB-поля. Затем следует удостовериться, что поток данных содержит более 4 байт информации (иначе в нем пе поместится даже «цифровая подпись»). После этого производится чтение «цифровой подписи» и при ее совпа- дении со значением константы Signature производится загрузка данных и установка свойства Modified равным False, что позволяет корректно определить, были ли произведены изменения содержимого OLE-коптейпера после загрузки данных. procedure TForml,TablelAfterScroll(DataSet: TDataSet): var Stream: TMemoryStream; N: Integer; begin 01eContainert.DestroyObject: Stream : = nil; try Stream := TMemoryStream.Create: TBLOBField(Tablel.FieldByName('Graphic')). SaveToStream(Stream): Stream.Seek(0. soFromBegi nni ng); if Stream.Size > 4 then begin Stream.ReadCN, SizeOf(N)); if N = Signature then OleContainerl.LoadFromStream(Stream); end; 01eContainerl.Modified ;= False: finally if Assigned(Stream) then Stream,Free: end; end:
246 Глава 5. Использование OLE-документов в приложениях Создание OLE-контейнера в виде чувствительного к данным VCL-компонента Приведенный выше пример имеет ряд недостатков. Так, если внести изменения в объект, содержащийся в OLE-контейнере, а затем закрыть приложение, то содер- жимое контейнера не будет сохранено в таблице. Дело в том, что при редактиро- вании объектов в OLE-контейнере компонент Tablel не информируется о проис- ходящих изменениях, поэтому не выставляется флаг, свидетельствующий о том, что данная запись пользователем изменена. Соответственно, на компоненте TDBNavigator недоступны кнопки Post и Cancel. Выход в данной ситуации заключается в создании на базе TOIeContainer компо- нента, чувствительного к данным (data-aware component). В компонентах такого типа должен быть создан объект TDataFieldLink. Этот объект связывается с источ- ником данных и каким-либо определенным полем из таблицы. Он имеет собы- тие OnDataChange, происходящее всякий раз, когда новые данные считываются из таблицы. В обработчике этого события данные помещаются в OLE-контейнер. Другое событие — OnllpdateData — вызывается для считывания совершенных из- менений. Это событие вызывается только в том случае, если либо ранее был вы- зван метод Edit объекта TDataFieldLink, переводящий текущую запись в состоя- ние редактирования; либо свойство Modified равно True. Последнее говорит о том, что в записи были сделаны изменения. Поскольку компонент TOIeContainer не имеет события OnChange (как, напри- мер, компонент TEdit), то метод Modified следует вызывать при активации OLE- контейнера, а также при выполнении метода InsertObjectDi al од и при очистке содержимого OLE-контейнера. Соответственно, эти два метода в компоненте TDBO1 eContainer перекрыты. Кроме того, чувствительные к данным компоненты обязаны откликаться па сообщение CM_GETDATALINK и возвращать источник данных. Исходный текст компонента TDBO1 eContainer приведен ниже: unit DBO1eContainer: interface uses Windows. Messages. Syslltils. Classes, Graphics, Controls, Forms. Dialogs. OleCtnrs, DB, DBCtrls; type TDBO1eContainer = classITOleContainer) private FDataLink: TFieldDataLink; FAutoDisplay: Boolean: FFocused: Boolean: FObjectLoaded: Boolean; FDummy: Integer: FFromActivate: Boolean: procedure DataChangelSender: TObject):
Хранение OLE-объектов в базах данных 247 function GetDataField: String; function GetDataSource: TDataSource: function GetField; TField; function GetReadOnly: Boolean; procedure SetDataField(const Value: String); procedure SetDataSource(Value: TDataSource): procedure SetReadOnly(Value: Boolean); procedure SetAutoDisplay(Value: Boolean); procedure SetFocused(Value: Boolean); procedure (JpdateData (Sender: TObject); procedure CMEnter(var Message: TCMEnter); message CM_ENTER; procedure CMExit(var Message: TCMExit): message CM_EXIT; procedure WMLButtonDblClk(var Message: TWMLButtonDNClk); message WMJ.BUTTONDBLCLK; procedure CMGetDataLink(var Message: TMessage): message CM_GETDATALINK; procedure DoDeactivate(Sender:TObject); protected procedure Loaded; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure LoadObject: vi rtual: public constructor Create(AOwner: TComponent); override; destructor Destroy; override: property Field: TField read GetField: functi on InsertObjectDi a1og:boolean: procedure DestroyObject; publ1 shed property DataSource:TDataSource read GetDataSource write SetDataSource: property DataField: String read GetDataField write SetDataField; property Readonly: Boolean read GetReadOnly write SetReadOnly default False: property AutoDisplay: Boolean read FAutoDisplay write SetAutoDisplay default True; property AutoActivate:integer read FDummy: end; procedure Register; implementation const Signature: Integer = -525465623;
248 Глава 5. Использование OLE-документов в приложениях constructor TDBOLEContainer.Create(AOwner: TComponent); begin inherited Create(AOwner); inherited AutoActivate := aaDoubleClick: Control Style := Control Style + [csReplieatable]: FAutoDisplay := True: FDataLink := TFieldDataLink.Create: FDataLink.Control := Self: FDataLink.OnDataChange := DataChange: FDataLink.OnUpdateData := UpdateData: OnDeactivate : = DoDeactivate: end: destructor TDBOLEContainer.Destroy; begin FDataLink.Free; FDataLink := nil; inherited Destroy: end; procedure TDBOLEConta i ner. Loaded: begin inherited Loaded: if (csDesigning in Componentstate) then DataChange(Self); end; procedure TDBOLEContainer.Notification(AComponent: TComponent: Operation: TOperation): begin inherited Notification(AComponent. Operation); if (Operation = opRemove) and (FDataLink <> nil) and (AComponent = DataSource) then DataSource := nil; end: procedure TDBOLEContainer,DoDeactivate(Sender:TObject): begin if Modified then begin if not FDataLink.Editing then FDataLink.Edit; FDataLink.Modi fled; end; end: functi on TDBOLEConta i ner.GetDataSource:TDataSource; begin Result := FDataLink.DataSource: end:
Хранение OLE-объектов в базах данных 249 procedure TDBOLEContaiпег.SetDataSource(Value: TDataSource): begin FDataLink.DataSource := Value: if Value <> nil then Value.FreeNotification(Self): end: function TDBOLEContainer.GetDataField: String; begin Result = FDataLink.FieldName: end: procedure TDBOLEContainer.SetDataField(const Value: String): begin FDataLink.FieldName := Value; end; functi on TDBOLEConta i ner.GetReadOnl у:Bool ean; begin Result := FDataLink.Readonly; end: procedure TDBOLEContainer.SetReadOnly(Value: Boolean): begi n FDataLink.Readonly := Value: if Value then inherited AutoActivate := aaDoubleClick else inherited AutoActivate := aaManual: end: function TDBOLEContai ner.GetFi eld:TFiel d: begin Result := FDataLink.Field: end; procedure TDBOLEContainer.LoadObject: var Stream: TMemoryStream: N: Integer: begin if not FObjectLoaded and Assigned(FDataLink.Field) and FDataLink.Field.IsBlob then begin i nheri ted Dest royObj ect: Stream : = nil; try Stream := TMemoryStream.Create: // создаем поток и сохраняем в нем данные из BLOB-поля ТВ1obFi eld(FDataLi nk.Fi eld).SaveToStream(Stream): Stream.Seek(0. soFromBeginning);
250 Глава 5. Использование OLE-документов в приложениях if Stream.Size > 4 then begin // если размер потока меньше 4, BLOB-поля в потоке нет Stream.Read(N, SizeOf(N)): if N = Signature then LoadFromStream(Stream); end; if Assigned(Stream) then begin Stream.Free; Stream := nil; end; FObjectLoaded := True: except on E: Exception do begin if Assigned(Stream) then Stream.Free; MessageDlg(E.Message. mtError, [mbOK], 0); end; end; Modified ;= False; end; end; procedure TDBOLEConta i ner.DataChange(Sender: TObject); begin if (FDataLink.Field <> nil) then if FDataLink.Field.IsBlob then begin if FAutoDisplay or (FDataLink.Editing and FObjectLoaded) then begin FObjectLoaded := False: LoadObject; end else begin FObjectLoaded := False; end; end; if HandleAl1ocated then RedrawWindow(Handle, nil. 0. RDWJNVALIDATE or RDW_ERASE or RDWJRAME): end: procedure TDBOLEContainer.UpdateData(Sender; TObject): var Stream: TMemoryStream; begin 11 Читаем OLE-данные из контейнера if FDataLink.Field.IsBlob then begin Stream := nil: try Stream ;= TMemoryStream.Create; Stream.Write(Signature. SizeOf(Signature)); if Assigned(01eObjectinterface) then SaveToStream(Stream);
Хранение OLE-объектов в базах данных 251 Stream.Seek(O. soFromBeginning): TBlobField(FDataLink.Field).LoadFromStream(Stream); if Assigned(Stream) then begin Stream.Free: Stream := nil; end; Modified := False: except on E: Exception do begin if Assigned(Stream) then Stream.Free: MessageDlg(E.Message. mtError, [mbOK], 0); end: end: end: end: procedure TDBOLEContainer.SetFocused(Value: Boolean); begi n if FFocused <> Value then begin FFocused := Value; if not Assigned(FDataLink.Field) or not FDataLink.Field.IsBlob then FDataLink.Reset: end; end: procedure TDBOLEContainer.CMEnter(var Message: TCMEnter); begin if FFromActivate then begin inherited; Exit: end: SetFocused(True): inherited; end; procedure TDBOLEContainer.CMExit(var Message: TCMExit): begin if FFromXctivate then begin inherited: Exit: end; try FDataLi nk.UpdateRecord; except SetFocus: raise: end;
252 Глава 5. Использование OLE-документов в приложениях SetFocused(False): inherited; end; procedure TDBOLEContainer.SetAutoDisplay(Value: Boolean); begin if FAutoDisplay <> Value then begin FAutoDisplay ;= Value: if Value then LoadObject: end; end; procedure TDBOLEContainer.WMLButtonDblCl k( var Message; TWMLButtonDNClk); begin if Assigned(OleObjectlnterface) then try FFromActivate := True; FDataLink.Edit; inherited; FDataLink.Modi tied: finally FFromActivate := False: end else try FFromActivate := True; FObjectLoaded := True: FDataLink.Edit; if inherited InsertObjectDialog then FDataLink.Modified; finally FFromActivate := False; end; end; procedure TDBOLEContainer.CMGetDataLink(var Message: TMessage): begin Message.Result : = Integer(FDataLink); end: function TDBOLEContainer.InsertObjectDialog:boolean; begin Result := False; try FFromActivate := True: FObjectLoaded := True: FDataLink.Edit: Result := inherited InsertObjectDialog: if Result then FDataLink.Modi tied: finally
Хранение OLE-объектов в базах данных 253 FFromActivate := False: end; end: procedure TDB01eContainer.DestroyObject: begin FDataLink.Edit: inherited DestroyObject: FDataLink.Modi tied: Invalidate: end: procedure Register: begi n RegisterComponents(’Util'. [TDBO1eContainer]); end: end. После создания компонента TDBO1 eContai ner его следует установить на палитру компонентов, выбрав в меню Delphi команду Component ► Install component. В ре- зультате на палитре компонентов будет создана страница Util, где и окажется но- вый компонент. Далее его можно использовать точно так же, как и любой другой компонент со страницы DataControls. Для тестирования компонента можно создать проект, аналогичный преды- дущему, по с компонентом TDBO1 eContaiпег. Обратите внимание, что использо- вание компонента, чувствительного к данным, приводит к тому, что данные видны па этапе разработки, как и в случае стандартных компонентов DataControl s (рис. 5.12). Рис. 5.12. Приложение с TDBOIeContainer на этапе разработки
254 Глава 5. Использование OLE-документов в приложениях Создадим для кнопок соответствующие обработчики событий: procedure TForml.ButtonlClick(Sender: TObject): begin if DBOleContainerl.InsertObjectDialog then i f Assigned(DBOLEContai nerl.DataSource) then DBOLEContainerl.DataSource.Edit; end; procedure TForml.Button2Click(Sender: TObject); begi n DBOLEConta i ner1.DestroyObject; if Assigned(DBOLEContainerl.DataSource) then DBOLEContainerl.DataSource.Edit; end: Таким образом, мы получили приложение, позволяющее сохранять OLE-объ- екты в базах данных и свободное от указанных выше недостатков. Заключение В этой главе мы познакомились с использованием OLE-документов в приложе- ниях. Мы изучили применение компонента TOIeContainer для работы с серверами документов в приложениях. Мы узнали, что: OLE-документы — это внедренные или связанные объекты, созданные прило- жениями, выступающими в качестве СОМ-серверов; для создания и использования OLE-документов в приложениях Delphi пред- назначен компонент ToleContainer; отображать в подобных компонентах можно в принципе любой СОМ-сервер, обладающий пользовательским интерфейсом, в частности многие из элемен- тов управления ActiveX; я для отображения панелей инструментов и меню OLE-серверов, обслуживающих отображаемые в TOIeContainer объекты, нужны компоненты TPanel и TMainMenu соответственно. Мы познакомились с различными способами вставки данных в OLE-контейнер, в частности с использованием диалогового окна Insert Object, методов CreateObject и PasteSpecial Di al og компонента TOIeContainer, а также с получением сведений об объекте с помощью метода ObjectPropertiesDiа 1 од. Мы обсудили вопросы управления объектом, помещенным в компонент TOIeContainer. Мы узнали, что: Ж с таким объектом с помощью метода DoVerb можно выполнить набор действий (обычно это отображение содержимого и редактирование с помощью соответ- ствующего сервера), доступных через свойство Objectverbs; если объект в OLE-контейнере экспонирует какие-либо свойства и методы, доступ к ним может быть осуществлен с помощью свойства OleObject компо- нента Т01 eContainer;
Заключение 255 используя свойство 01 eObject компонента TOIeContainer, мы можем реализо- вать действия, доступные контроллерам автоматизации соответствующего сер- вера OLE-документов за исключением его удаленного запуска. Мы обсудили часто встречающуюся на практике проблему храпения OLE- объектов в базах данных. Мы узнали, что: N хранить OLE-объекты можно практически в любой базе данных — для этой цели могут быть использованы любые BLOB- и МЕМО-поля; основная идея приложения, сохраняющего содержимое OLE-контейнера в базе данных, заключается в том, что объект, содержащийся в компоненте TOIeContainer, сохраняется в файле или памяти, а затем помещается в BLOB-поле. Мы рассмотрели несколько возможностей реализации подобных приложе- ний — использование временного файла, использование данных в памяти и ме- тодов-потомков класса TDataSet и, наконец, реализацию OLE-коптейпера в виде VCL-компонента, чувствительного к данным. Итак, мы уже научились создавать и применять элементы управления ActiveX, разрабатывать локальные серверы автоматизации и их контроллеры, применять в приложениях OLE-документы и сохранять их в базах данных, то есть решать относительно простые задачи, встречающиеся практически каждому пользова- телю Delphi. Однако вопросы применения технологии СОМ этим отнюдь не исчерпываются, и, начиная со следующей главы, мы продолжим знакомство с этой технологией па более глубоком уровне.
ГЛАВА 6 Модели потоков и разработка многопоточных приложений Для лучшего понимания вопросов вычислений в потоках необходимо совер- шить исторический экскурс и проанализировать развитие способов вычисления. Вследствие несовершенства технологии сборки первые созданные компьютеры имели значительную стоимость, поэтому сразу же встала задача увеличения их загрузки, что достигалось обработкой заданий в пакетном режиме — после завер- шения какой-либо задачи (или прерывания ее по времени) взамен загружалась другая задача, и расчеты продолжались уже для нее. По мере развития техноло- гий сборки и удешевления компьютерного времени такой способ оказался неэф- фективным: если задача завершалась с кодом ошибки, то программист узнавал об этом не сразу, причем после внесения изменений и повторной постановки задачи в очередь не было никаких гарантий отсутствия других ошибок. Таким образом, для отладки приложений требовалось значительное время. Чтобы изменить эту ситуацию, были созданы операционные системы (UNIX), которые могли работать сразу с несколькими задачами. Все эти задачи загружались в память компьютера, и процессор начинал расчет для первой задачи. Затем, спустя некоторое время, запоминались состояния регистров процессора и их значения заменялись данными для следующей задачи. Опять же, спустя некоторое время, в регистрах процес- сора восстанавливались данные для первой задачи, и расчет продолжался для нее. Если такое переключение происходило достаточно часто, то у пользователя создавалась иллюзия, что компьютер одновременно решает несколько задач. Такой механизм обработки задач называется вытесняющей многозадачностью (preemptive multitasking). Его реализация позволила использовать терминалы, напрямую подключенные к компьютеру, и, таким образом, существенно повы- сить эффективность труда программистов. С появлением дешевых персональных компьютеров, казалось бы, надобность в многозадачности отпала, во всяком случае, операционная система DOS могла работать только в пакетном режиме. Однако оказалось, что это не так: пользова- тель часто хотел редактировать данные и при этом проводить какие-либо расчеты в фоновом режиме. Поэтому операционная система Windows 3.1 уже была созда- на как многозадачная: пользователь мог запустить одновременно несколько приложений. Однако операционная система Windows 3.1 не вытесняла задачи: переход к расчету для следующей задачи мог осуществляться только после за- вершения расчетов для предыдущей, когда очередь сообщений была свободна. Такой механизм расчетов называется невытесняющей многозадачностью (поп-
Класс TThread 257 preemptive multitasking). Если какая-либо задача входила в бесконечный цикл (или просто требовала значительного времени для своего решения), то пользователь пе мог переключиться на другую задачу. Такая ситуация называется захватом процессора. Вытесняющая многозадачность была реализована в Windows, начи- ная с 32-разрядных версий (95/98/NT/2000/XP). Если операционная система поддерживает вытесняющую многозадачность, то почему бы пе сделать следующий шаг, а именно: в рамках одной задачи заставить параллельно выполняться несколько различных участков исполняемого кода? Такой режим работы называют многопоточным (multithreading) и в этом случае говорят, что каждый участок кода выполняется в отдельном потоке. К сожалению, в русском языке термин «ноток» используется также для обозначения потоков данных, поэтому там, где это необходимо, мы будем уточнять, о каком именно потоке идет речь: потоке выполнения (thread) или потоке данных (stream). В на- стоящей главе речь пойдет о потоках выполнения. Хотя программист может создать сколько угодно потоков, па компьютерах с одним процессором пе рекомендуется создавать более 16 потоков. Переключение между потоками занимает заметное время. Если, например, суммировать числа в массиве из 16 000 000 элементов в одной задаче и суммировать числа в 16 пото- ках в массивах из 1 000 000 элементов, то время выполнения второй задачи будет заметно больше. Однако при наличии нескольких процессоров разные потоки выполняются па разных процессорах, и время решения задачи существенно со- кращается. Класс TThread В Delphi вычисления в потоках реализуются при помощи абстрактного класса TThread, для чего необходимо создать класс-потомок и при помощи директивы override перекрыть абстрактный метод Execute: type TMyThread = class(TThread) protected procedure Execute: override; end: procedure TMyThread.Execute: begin {Program code} end; Код, который реализуется в методе Execute, выполняется в отдельном потоке. Для его запуска просто необходимо создать экземпляр класса TMyThread: ТМуThread.Create(False): COM-объекты самостоятельно, без использования класса TThread, поддержи- вают вычисления в потоках. Однако этот класс полезно рассмотреть с точки зрения анализа сложностей, возникающих при создании многопоточных приложений.
258 Глава 6. Модели потоков и разработка многопоточных приложений При запуске любого приложения автоматически создается отдельный поток, который называют главным. В коде главного потока реализуется прием сообщений Windows, прорисовка графики па экране, обработка событий (если обработчики специально не созданы так, чтобы выполняться в отдельном потоке). Создание экземпляра класса-потомка TThread означает, что код в методе Execute будет вы- полняться параллельно с кодом главного потока, периодически прерывая выпол- нение кода главного потока, — создается фоновый поток. Он может находиться в одном из двух состояний: ожидание и выполнение. Если фоновый поток нахо- дится в состоянии ожидания, то процессор ему просто не выделяет время. При этом все время отдается другим потокам. Соответственно, класс TThread имеет два метода: в Suspend — переводит работающий поток в состояние ожидания; Ш Resume — переводит ожидающий поток в рабочее состояние. Конструктор класса TThread принимает в качестве параметра логическую пе- ременную CreateSuspended. Если ее значение равно True, то конструктор полно- стью отрабатывается, по поток находится в состоянии ожидания. Для его запуска требуется вызов метода Resume. При значении этого параметра, paBiiOiM False, код в методе Execute начинает выполняться немедленно после отработки конст- руктора. Нельзя вызывать конструктор класса-потомка TThread с параметром Fa 1 se, если происходит инициализация свойств в классе: type TMyThread = class(TThread) protected procedure Execute: override: public FR: Single: end: procedure TMyThread.Execute; var R: Single: begin R := Sqrt(FR - 1): end; procedure TForml.ButtonlCl1ck(Sender:TObject): begin with TMyThread.Create(False) do FR := 5; end: На первый взгляд этот код не содержит ошибки: создается отдельный поток, присваивается начальное значение переменной FR и продолжается расчет с ис- пользованием этого значения. Однако поскольку после отработки конструктора код, реализованный в методе Execute, выполняется в отдельном потоке, то оператор
Класс TThread 259 R := sqrt(FR-l) из метода TMyThread.Execute может быть выполнен раньше опера- тора FR := 5 из метода TForml.ButtonlClick. Соответственно, значение переменной FR после отработки конструктора равно 0, и в такой ситуации будет происходить исключение Elnval 1 dOp (попытка извлечь квадратный корень из -1). Поиск и устра- нение такого типа ошибки усложняются тем, что, вообще говоря, эта ошибка не- постоянна и в некоторых случаях может проявляться очень редко. Для устране- ния указанной ошибки код в методе ButtonlCl ick следует переписать: procedure TForml.ButtonlCl1ck(Sender:TObject); begin with TMyThread.Create(True) do begin FR := 5; Resume; end; end; В приведенном фрагменте кода первоначально создается экземпляр класса TMyThread в режиме ожидания, затем присваиваются начальные значения всем свойствам и переменным класса и только после этого вызывается метод Resume, который начинает выполнение кода. Это единственно правильный способ ини- циализации данных: поток не запускается до тех пор, пока не заданы значения всех переменных. В классе TThread также определено свойство Priority, которое может прини- мать следующие значения: tpldle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest, tpTimeCritical. Это свойство определяет долю времени, которую процессор отво- дит для выполнения фонового потока по сравнению с главным потоком. При значении свойства Priority = tpNormal эти времена совпадают. Например, если 10 секунд времени было потрачено па решение задачи, то 5 секунд процессор вы- делил главному потоку и 5 секунд фоновому. Поток с приоритетом tpldle прак- тически не мешает выполнению главного потока (хотя процессор все же выделяет время па его выполнение). Поток с таким приоритетом можно использовать, например, для проверки данных в буфере обмена и в зависимости от их измене- ний делать доступными или недоступными кнопки. Другая крайность — приори- тет tpHighest — время для работы главного потока практически не остается. Если в потоке с таким приоритетом выводить текущее значение счетчика на экран, то оно отображаться не будет — до того как графика успеет отобразиться па экране, фоновый поток прервет главный поток, чтобы обновить значение счетчика. Несколько слов о потоках с приоритетом tpTimeCritical. Это наивысший при- оритет. Создание приложений, использующих потоки с таким приоритетом, тре- бует соблюдения определенных правил. Дело в том, что некоторые методы ядра Windows являются асинхронными, по выполняются с очень большим приорите- том. Приоритет tpTimeCritical сравним с приоритетом вызова этих методов. Это означает, что при вызове такого метода его выполнение может не успеть завер- шиться до выполнения следующего оператора из созданного приложения. Имеется список функций ядра, которые нельзя использовать в приложениях с приоритетом tpTimeCritical. В традиционных приложениях такой приоритет не нужен, по он может потребоваться в некоторых играх при показе видеоизображений.
260 Глава 6. Модели потоков и разработка многопоточных приложений Завершая разговор о приоритетах, следует предостеречь от попыток с их по- мощью синхронизировать время завершения работы потоков. Предположим, име- ется два фоновых потока, причем при одинаковом приоритете они имеют срав- нимое время завершения. Предположим, что для завершения работы одного из этих потоков требуются результаты расчетов, выполненные в другом потоке; то есть второй поток должен завершиться раньше первого. Казалось бы, первому потоку можно назначить приоритет tpLowest, а второму — tpHighest, и задача будет решена. Это абсолютно верные рассуждения, по только при условии проведения вычислений па однопроцессорных компьютерах. При переходе же па многопро- цессорные системы эти две задачи будут выполняться разными процессорами, и приоритет перестанет играть роль — каждый поток займет максимально воз- можное время процессора. Для синхронизации процессов необходимо использо- вать сигнальные объекты — о них будет сказано далее в этой главе. Следует отметить свойство FreeOnTerminate класса TThread. Если его значение равно True, то после завершения метода Execute автоматически вызывается дест- руктор. При значении же этого свойства, равном False, деструктор требуется вы- зывать в явном виде из кода приложения. Из других методов следует отметить метод Terminate. Вызов этого метода присваивает значение True свойству Terminated (только для чтения). Он пе прерывает код, описанный в методе Execute. Однако при написании метода Execute программист обязан достаточно часто проверять значение свойства Terminated, и если вдруг оно станет равным True, максимально быстро прекратить выполнение метода Execute. Никаких сообщений пользователю из потока пе выводится (это надо делать раньше, до вызова метода Terminate), достаточно только освободить системные ресурсы (если они были зарезервиро- ваны) и прекратить выполнение кода. Класс TThread имеет единственный обработчик событий OnTerminate, который вызывается при завершении метода Execute. В этом обработчике событий можно, например, узнать результаты расчетов, выполненных в потоке. Код при стан- дартном способе использования этого обработчика событий выглядит следую- щим образом: procedure TForml.ButtonlClick(Sender: TObject): begi n with TMyThread.Create(True) do begin FreeOnTerminate:=True; OnTerm inate:=ThreadDone: Resume: end: end: procedure TForml.ThreadDone(Sender: TObject): begin with Sender as TMyThread do begin 11 Здесь реализуется код для обработки результатов расчетов end: end:
Понятие о синхронизации 261 После завершения кода в потоке будет вызван метод ThreadDone, где можно прочитать результаты расчетов. Забегая немного вперед, отметим, что для чтения переменных класса TMyThread в методе OnTernrinate не требуется синхронизации. И, наконец, из фоновых потоков не должны возбуждаться исключения. При- ложение не может корректно обработать исключения, если они возбуждены вне главного потока. Поэтому весь код в методе Execute следует помещать в блок пе- рехвата исключения: procedure TMyThread.Execute: begin try // Выполняемый код здесь except // Запрещается использовать директиву raise в этом месте! II Возможные действия - возврат кода ошибки end: end: Ситуация с исключениями осложняется тем, что они корректно обрабатыва- ются отладчиком Delphi и поэтому в режиме отладки не воспроизводятся. Понятие о синхронизации В комплект поставки Delphi 7 входит пример использования класса TThread — сортировка массива случайных чисел по трем разным алгоритмам. Его можно найти в каталоге Delphi7\Demos\Threads. В процессе сортировки промежуточные значения выводятся па экран, и пользователь может наблюдать за сортировкой массивов (рис. 6.1). Рис. 6.1. Результат сортировки случайного массива по разным алгоритмам
262 Глава 6. Модели потоков и разработка многопоточных приложений Проанализируем теперь, как графика выводится на экран. Открыв файл SortThds.pas, можно найти метод DoVisualSwap, ответственный за прорисовку изо- бражения во время выполнения приложения. Ничего необычного в самом методе нет, необычно то, как он вызывается для выполнения прорисовки. Это происхо- дит с использованием метода Synchronize в методе Visua 1 Swap потока: Synchronize(DoVisualSwap); Метод Synchronize определен в классе TThread и в качестве параметра он при- нимает адрес другого метода, который, в свою очередь, не должен содержать па- раметров и должен быть объявлен как процедура. Для понимания того, что он делает, вызовем метод DoVisualSwap без вызова Synchronize: = А = В = I = J procedure TSortThread.VisualSwapCA, В. I. J: Integer); begin FA FB FI FJ DoVisualSwap; end; Если запустить данное приложение, щелкнуть на кнопке Start Sorting и в про- цессе его выполнения водить указателем мыши ио форме с графическим изобра- жением, то периодически в графическом изображении будут наблюдаться де- фекты (рис. 6.2). Рис. 6.2. Дефекты графического изображения при отсутствии синхронизации Этот тест хорошо выполняется па платформах Windows 95/98/2000 и плохо на платформе Windows NT, в которой редко происходит переключение между потоками. Более того, если данный тест выполнять из Delphi в режиме отладки,
Понятие о синхронизации 263 то периодически будет появляться исключение Ell 1 egal Operation со следующим сообщением: Canvas does not allow drawing Объяснение этого теста заключается в том, что при работе с потоками один поток прерывает выполнение другого в произвольный момент времени, в том числе и самый неподходящий с точки зрения программиста. Если при этом оба конкурирующих потока (или хотя бы один из них) производят модификацию какой-либо общей переменной, то при программировании таких приложений не- обходимо применять специальные меры, которые называются синхронизацией. В данном примере происходит прорисовка графики из потоков, которые осуще- ствляют сортировку. Кроме того, из основного потока происходит прорисовка указателя мыши в новом положении. Все потоки обращаются к общей области памяти, содержимое которой выводится па экран. Рассмотрим временную диа- грамму, которая объяснит появление дефектов при отсутствии синхронизации. 1. Указатель мыши смещается в новое положение. При этом операционная сис- тема запоминает участок экрана, на котором следует выполнить прорисовку указателя мыши. 2. Какой-либо из потоков прерывает выполнение главного потока и прорисовы- вает па форме приложения новые диаграммы с результатами сортировки. 3. Выводится указатель мыши. При следующем перемещении указателя восста- навливается первоначальное изображение, которое пе содержит изменений, произошедших в п. 2. В результате появляется дефект. На самом деле обращение к графической памяти — процесс гораздо более сложный, чем описано выше. При обращении происходит инициализация пере- менных и изменение состояния объекта. Если потоки переключаются в процессе инициализации, то с большой вероятностью состояние объекта является неопре- деленным — например, графическая память пе готова к изменениям. Если в этот момент попытаться изменить содержимое памяти, то возбуждается упомянутое выше исключение EI11 egal Operation. Для лучшего понимания механизма синхронизации и обоснования необходимо- сти в пей рассмотрим следующий пример. Предположим, что имеется постоян- ный буфер в памяти, куда один из потоков заносит дату в текстовом виде, а другой считывает это значение и использует для каких-либо вычислений. Рассмотрим процесс, который реально может происходить при такой конфигурации. Начальное значение буфера: 13 December 1999 1. Поток, который должен считывать значение, начал чтение и считал первые 8 символов: 13 Decern 2. Происходит вытеснение первого потока, и второй поток заносит повое значение: 15 February 2000
264 Глава 6. Модели потоков и разработка многопоточных приложений 3. Через некоторое время восстанавливается первый поток, который продолжает чтение буфера с прерванного места и считывает оставшуюся часть: агу 2000 Значение даты, которое считал первый поток (13 Decemary 2000), абсолютно бес- смысленно. Если, например, занести данное значение в базу данных, то очевидны трудности с последующей интерпретацией этой строки. К сказанному следует добавить, что если хранить эту переменную не в постоянном буфере, а использо- вать переменную типа string, могут происходить нарушения в защите памяти. При изменении длины строки в п. 3 происходит перераспределение памяти, и при выполнении п. 4 описанного процесса может произойти обращение к области па- мяти, которая не содержит данных. Таким образом, если два или более потока обращаются к какой-либо общей переменной, то требуется написание специального программного кода, смысл которого состоит в том, что если какой-либо из потоков работает с общей пере- менной, то другие потоки не имеют права прервать выполнение этого потока до окончания работы с ней. Такой код обеспечивает синхронизацию доступа к дан- ным. Синхронизация не требуется, если из разных потоков происходит только чтение данных. Однако если хотя бы один из потоков изменяет данные, то требу- ется синхронизация как при чтении переменной, так и при ее записи. Метод Synchronize, который определен в классе TThread, обеспечивает синхрони- зацию фонового и главного потоков. В качестве параметра этот метод использует адрес другого метода — например, DoSomething. При выполнении кода, реализован- ного в методе DoSomething, главный поток не прерывает фоновый до завершения выполнения метода DoSomething. В коде DoSomething можно обращаться к общим переменным, не опасаясь описанных выше коллизий. Ясно, что метод Synchronize нельзя использовать для синхронизации доступа к данным из двух фоновых по- токов. Для этого требуются специальные объекты — критические секции, сема- форы, мыотексы, сообщения. О них будет рассказано ниже. Следует отметить существование еще одного вида синхронизации — синхро- низации процессов. Синхронизацию такого типа необходимо осуществлять, на- пример, когда для завершения работы одного из потоков необходимы результаты, полученные при расчете в другом потоке. Если таких результатов еще пет (на- пример, второй поток продолжает расчеты), то первому потоку необходимо подождать до завершения расчетов второго. Это является стандартной ситуацией при создании распределенных приложений — когда расчеты осуществляются на нескольких компьютерах. Для синхронизации процессов используются те же самые способы, что и для синхронизации данных. Если процессы выполняются в разных адресных пространствах, то для синхронизации нельзя использовать критические секции. В классе TThread определен метод WaitFor. Вызов этого метода означает пре- кращение выполнения кода главного потока до окончания работы фонового. Соз- дадим новый проект, поместим на форму две кнопки, объявим класс TMyThread — потомок класса TThread и реализуем следующий код:
Понятие о синхронизации 265 procedure TMyThread.Execute: begi n // Модель реального кода вычислений repeat Inc(FL); Sleep(lOO): Beep: until (FL >= 30) or Terminated: end: procedure TForml.ButtonlClick(Sender: TObject): begin if FS <> nil then Exit: FS := TMyThread.Create(True); with FS do begin FreeOnTerminate := True; OnTerminate := ThreadDone: Resume; end: end; procedure TForml.Button2Click(Sender: TObject): begin if FS <> nil then FS.WaitFor; ShowMessage('Done'); end: procedure TForml.ThreadDone(Sender: TObject); begin FS := nil: end; Данный код можно тестировать в операционной системе Windows 95/98/NT, по пе Windows 2000. При щелчке на первой кнопке начинаются вычисления — это контролируется па слух по писку динамиков. Если в процессе вычислений щелкнуть па второй кнопке, то сообщение «Done» появится не сразу, а только после окончания писка в динамиках. Вследствие ошибки в VCL при реализации метода TThread.WaitFor, а также изменений в операционной системе Windows 2000, вызов деструктора класса TThread осуществляется до того, как завершится код метода TThread.WaitFor. Это приводит к появлению исключения EOSExeption с кодом 6 — Invalid Handle. По- этому если может быть вызван метод WaitFor, то свойство FreeOnTerminate должно иметь значение False. В этом случае запрещено вызывать деструктор в явном виде из обработчика событий OnTerminate — он будет вызываться раньше завершения метода WaitFor.
266 Глава 6. Модели потоков и разработка многопоточных приложений Потоки и апартаменты Windows — многозадачная и многопоточная среда с вытесняющей многозадачно- стью. Применительно к СОМ это означает, что клиент и сервер могут оказаться в различных процессах или потоках приложения, а к серверу может обращаться множество клиентов, причем в непредсказуемые моменты времени. Технология СОМ решает эту проблему введением концепции апартаментов (apartments), в которых и выполняются СОМ-клиепты и COM-серверы. В главе 1 мы уже го- ворили о том, что апартаменты бывают одпопоточные (Single Threaded Apartment, STA) и многопоточные (Multiple Threaded Apartment, MTA). Напомним, чем они отличаются друг от друга. STA При создании одпопоточпого апартамента СОМ неявно создает окно и при вы- зове любого метода СОМ-сервера в этом апартаменте посылает окну сообщение при помощи функции PostMessage. Таким образом, организуется очередь вызовов методов, и каждый из них обрабатывается только после того, как обработаны все предшествующие вызовы. Ниже перечислены основные достоинства одпопоточ- пого апартамента. 9 Программист может не заботиться о синхронизации методов. Гарантируется, что до окончания выполнения текущего метода не будет вызван никакой дру- гой метод объекта. В Программист может не заботиться о синхронизации доступа к нолям класса, реализующего объект. Поскольку одновременно может выполняться только один метод, одновременный доступ к полю из двух методов невозможен. В то же время, если приложение создало несколько потоков, в каждом из которых имеется STA, при доступе к глобальным разделяемым данным для этих данных требуется синхронизация, например, с использованием критических секций. Недостатки одпопоточпого апартамента напрямую вытекают из его реализации. 9 Дополнительные (и иногда излишние) затраты па синхронизацию при вызове методов. Я Невозможность отклика па вызов метода, пока не исполнен предыдущий. На- пример, если текущим является метод, требующий одну минуту на исполне- ние, то до его завершения COM-объект будет недоступен Тем не менее STА, как правило, является наиболее подходящим выбором для реализации СОМ-сервера. Использовать МТА есть смысл только в том случае, если STA не подходит для конкретного сервера. МТА Многопоточный апартамент не реализует автоматически синхронизацию и свобо- ден от связанных с этим ограничений. Внутри пего может быть создано сколько угодно потоков и объектов, причем каждый объект не привязывается к какому-то
Потоки и апартаменты 267 конкретному потоку. Это означает, что любой метод объекта может быть вызван в любом из потоков МТА. В это же самое время в другом потоке может быть вы- зван любой другой (либо тот же самый) метод COM-объекта по запросу другого клиента. СОМ автоматически ведет пул потоков внутри МТА и при вызове со стороны клиента находит свободный поток и в нем вызывает метод нужного объ- екта. Таким образом, даже если выполнение метода требует длительного времени, для другого клиента он может быть вызван без задержки в другом потоке. Оче- видно, что COM-сервер, работающий в МТА, обладает потенциально более вы- соким быстродействием и доступностью для клиентов, однако он значительно сложнее в разработке, поскольку даже локальные данные объектов не защищены от одновременного доступа и требуют синхронизации. Нейтральный апартамент Для поддержки серверов СОМ+ в Windows 2000 добавлена еще одна модель по- токов — модель нейтральных потоков (neutral-threaded model) и соответствую- щий тин апартамента — нейтральный апартамент (neutral apartment). СОМ+ автоматически создает один такой апартамент па приложение для объектов, имеющих модель нейтральных потоков. Объекты с этой моделью при вызове клиентом, находящимся в одном процессе с сервером, вызываются в потоке клиента. Таким образом минимизируются затраты на переключение потоков. Модель ней- тральных потоков рекомендуется для компонентов СОМ+, не имеющих пользо- вательского интерфейса. Передача интерфейсов и параметров Таким образом, СОМ-клиепт н COM-сервер могут выполняться как в одном апартаменте, так и в разных, расположенных в различных процессах пли даже па разных компьютерах. Встает вопрос — как же клиент может вызывать методы сервера, если они находятся в общем случае в другом адресном пространстве? Эту работу берет па себя СОМ. Для доступа к серверу в другом апартаменте клиент должен запросить у СОМ создание в своем апартаменте представителя, реализующего запрошенный интерфейс. Такой представитель в терминах СОМ называется прокси (proxy) и представляет собой объект, экспортирующий запро- шенный интерфейс. Одновременно СОМ создает в апартаменте сервера стаб (stub), принимающий вызовы от прокси и транслирующий их в вызовы сервера. Таким образом, клиент в своем апартаменте может рассматривать прокси в каче- стве сервера и работать с ним так, как будто сервер создан в его апартаменте. В то же время сервер может рассматривать стаб как расположенного с ним в од- ном апартаменте клиента. Всю работу по организации взаимодействия между прокси и стабом берет па себя СОМ. При вызове со стороны клиента прокси по- лучает от него параметры, упаковывает их во внутреннюю структуру и передает в апартамент сервера. Стаб получает параметры, распаковывает их и произво- дит вызов метода сервера. Аналогично осуществляется передача параметров обратно. Эти процессы называются маршалингом (marshalling) и демаршалингом (unmarshalling) соответственно. При этом апартаменты клиента и сервера могут иметь разные модели потоков и физически находиться где угодно. Разумеется,
268 Глава 6. Модели потоков и разработка многопоточных приложений такой вызов означает значительные накладные расходы но сравнению с вызовом сервера в «своем» апартаменте, однако это единственный способ обеспечить кор- ректную работу любых клиентов и серверов. Чтобы избежать накладных расхо- дов, сервер надо создавать в том же апартаменте, в котором расположен клиент. Для корректного создания прокси в клиентском апартаменте СОМ требуется узнать «устройство» сервера. Сделать это можно несколькими способами. И Реализовать па сервере интерфейс IMarshal и при необходимости прокси в виде библиотеки DLL, которая будет загружена па клиенте. Подробности реализа- ции описаны в документации СОМ и MSDN. Я Описать интерфейс на языке IDL (Interface Definition Language — язык опре- деления интерфейсов) и при помощи компилятора MIDL корпорации Micro- soft сгенерировать библиотеку DLL, реализующую прокси и стаб. Я Сделать сервер совместимым с технологией OLE Automation. В этом случае СОМ самостоятельно создает прокси, используя описание сервера из его библиотеки типов — специального двоичного ресурса, описывающего СОМ- иптерфейс. При этом в интерфейсе допустимы только совместимые с OLE Automation типы данных. Инициализация СОМ Каким же образом клиенты и серверы СОМ могут создавать апартаменты в соот- ветствии со своими требованиями? Для этого они должны соблюдать одно пра- вило: каждый поток, который желает использовать СОМ, должен создать апарта- мент путем вызова функции CoInitializeEx. Опа объявлена в модуле ActiveX.pas следующим образом: const COINITMULTITHREADED = 0; COINIT_APARTMENTTHREADED = 2; COINIT_DISABLE_OLE1DDE = 4; COINIT_SPEED_OVER_MEMORY = 8; function CoInitializeEx(pvReserved: Pointer; colnit: Longint): HResult; stdcall; Параметр pvReserved зарезервирован для будущего использования и должен быть равен nil, а параметр colnit определяет модель потоков создаваемого апар- тамента. Он представляет собой комбинацию из следующих флагов: Я COINIT_APARTMENTTHREADED — для потока создается однопоточный апартамент; каждый поток может иметь (или пе иметь) свой апартамент; Я COINIT_M(JLTITHREADED — если в текущем процессе еще пе создан многопоточный апартамент, он создается, если же многопоточный апартамент уже создан дру- гим потоком, текущий поток «подключается» к готовому апартаменту, иными словами, каждый процесс может иметь только один многопоточный апартамент; Я COINIT_DISABLE_OLE1DDE — запрещает использование протокола DDE для под- держки OLE версии 1;
Потоки и апартаменты 269 COINIT_SPEED_OVER_MEMORY — оптимизирует скорость выполнения за счет боль- шего расхода памяти. Функция возвращает значение S_OK в случае успешного создания апартамента. По завершении работы с СОМ (или перед завершением работы) поток дол- жен уничтожить апартамент путем вызова процедуры CoUni initialize, также опи- санной в модуле ActiveX: procedure CoUninitialize; stdcall; Каждому вызову CoInitializeEx должен соответствовать вызов CoUninitialize, то есть если вы используете СОМ в приложении, вы должны вызвать процедуру CoInitializeEx до первого вызова функций СОМ и CoUninitialize перед заверше- нием работы приложения. Библиотека VCL выполняет автоматическую инициа- лизацию СОМ при использовании модуля ComObj. По умолчанию создается STA. Если вы хотите задействовать другую модель потоков, следует установить флаг инициализации СОМ до оператора Application.Initialize: program Projectl; uses Forms. ComObj, ActiveX. Unitl in 'Unitl.pas' {Forml}; {$R ★.RES} begin CoInitFlags := COINIT_MULTITHREADED; Application.Initialize: Applicati on.CreateFormlTForml, Forml); Appl1cation.Run; end. Если COM используется в потоке, то эти функции должны быть вызваны в ме- тоде Execute: procedure TMyThread.Execute; begin CoInitializeEx(nil, COINIT_MULTITHREADED): CoUninitialize: end; Отдельного обсуждения заслуживает инициализация модели потоков СОМ для сервера, реализованного в виде DLL. Дело в том, что библиотека DLL может быть загружена любым потоком, который уже ранее создал свой апартамент. По- этому сервер в виде DLL не может сам проипициализировать требуемую ему мо- дель потоков. Вместо этого сервер при регистрации прописывает в реестре пара- метр ThreadingModel, который и указывает, в какой модели потоков способен
270 Глава 6. Модели потоков и разработка многопоточных приложений работать данный сервер. При создании сервера СОМ анализирует значение этого параметра и при необходимости создает для сервера апартамент с требуемой моделью потоков (рис. 6.3). tgistry Editor Registry Edit View Favorites Help : Ж C81060AF76G68DD41D0-8FC1-00C04FD9189Df2 j 8) £j {0618AA30-6BC4-11CF-BF36-00AA0055595A} • \ C] {06210E83-01F5-HD1-B512-0080C781C334} i Й £j {06290BDO-48AA-11D2-8432-006008C3FBFC} H = S? Cj {062908D1-48AA-11D2-8432-006008C3F8FC} </ . • ГЙ Q {06290BD2-48AA-11D2-8432-006008C3FBFC} • : : 1*1 Cd {062908D3-48AA-11D2-8432-006008C3FBFCJ- \ : Й Cj {06290BD4-48AA-11D2-8432-006008C3FBFC} s ? ! Cl Implemented Categories €j Inproc5erver32' i Type i Qata tone , .^(Default) REG.52 ^ThreadingM lei REG_5Z C:\VVINNT\System32\scrobj. dll Apartment IM-J {0629rjB05-48AA41D2-8432-006008C3FBFC}- i I&CJ {06290808-48AA-11D2-8432-006008C3FBFC} I It; £j {06290BD9-48AA-1102-8432-006008C3FBFC)- ? ffi Q {06290BDA-48AA-11D2-8432-006008C3FBFC)- ; : Itl £j {06290BDB-4SAA-11D2-8432-006008C3FCFC} •»1 14 lixooincc топ itm ллллглоипгпг» \ My Computer\HK£V aASSES ROOr1CLS10\-(06:'»B0-4-4eAA-l it>2-8«M • FCHlnprocS, г. Рис. 6.3. Запись в системном реестре, определяющая модель потоков внутрипроцессного сервера Параметр Thread! ngModel может принимать следующие значения: в Single — сервер не предоставляет поддержки потоков, СОМ упорядочивает запросы клиентов к приложению, в каждый момент времени может обслужи- ваться не более одного запроса; Я Apartment — сервер может работать только в STA, если он создается из STA, то появится в апартаменте вызывающего потока, если из МТА — СОМ автома- тически создаст для пего одпопоточпый апартамент и прокси в апартаменте клиента; 8 Free — сервер может работать только в МТА, если он создается из МТА, то появится в апартаменте вызывающего потока, если из STA — СОМ автомати- чески создаст для пего многопоточный апартамент и прокси в апартаменте клиента; в Both — сервер может работать как в ST А, так и в МТА, объект всегда создается в вызывающем апартаменте; Я Neutral — сервер поддерживает модель нейтральных потоков, объект создается в специальном апартаменте, который, в свою очередь, создается автоматиче- ски (обратите внимание на невозможность создания апартамента с моделью нейтральных потоков при помощи функции Coinitialize). Если параметр Thread! ngModel не задан, сервер по умолчанию получает модель одного потока (см. ниже). В этом случае он создается в главном (primary) одпо- ноточном апартаменте (то есть в ST А потока, который первым вызвал процедуру
Потоки и апартаменты 271 Coinitialize), даже если создание сервера запрошено из потока, имеющего собст- венный однопоточпый апартамент. При создании СОМ-сервера средствами Delphi его модель потоков задается в мастере COM Object Wizard (рис 6.4). Рис. 6.4. Выбор модели потоков на этапе создания СОМ-сервера В главе 1 мы уже обсуждали, как зависит реализация создаваемого СОМ-объ- екта от параметров, заданных в этом окне. Здесь же мы подробно обсудим значе- ния, доступные в раскрывающемся списке Threading Model и определяющие мо- дель потоков сервера (действие выбранного значения зависит от типа сервера — EXE или DLL). Я Single — модель одного потока (single-threaded model). Нет поддержки потоков. Для DLL-сервера при регистрации не будет создан параметр Thread! ngModel. Для ЕХЕ-сервера выбор этого значения (в отличие от любого другого) не приведет к установке флага IsMultiThread, поэтому будет создан одпопоточ- пый апартамент. Обычно эта модель используется для внутренних серверов. Apartment — модель разделенных потоков (apartment-threaded model). Для DLL-сервера в реестре будет создан параметр Thread!ngModel, равный значе- нию Apartment, для ЕХЕ-сервера будет создан однопоточпый апартамент. Я Free — модель свободных потоков (free-threaded model). Для DLL-сервера в ре- естре будет создан параметр Thread!ngModel, равный Free, для ЕХЕ-сервера будет создан многопоточный апартамент. » Both — модель смешанных потоков (both-threaded model). Для DLL-сервера в реестре будет создан параметр Thread!ngModel, равный Both, для ЕХЕ-сервера будет создан многопоточный апартамент. К Neutral — модель нейтральных потоков (neutral-threaded model). Для DLL- сервера в реестре будет создан параметр Thread!ngModel, равный Neutral, для ЕХЕ-сервера будет создан многопоточный апартамент.
272 Глава 6. Модели потоков и разработка многопоточных приложений При выборе значения в раскрывающемся списке Threading Model пе предприни- мается никаких дополнительных действий по обеспечению корректности работы создаваемого сервера в выбранной модели потоков. Поэтому вы сами должны продумать, какие потоки могут быть созданы в вашем приложении, и предпри- нять меры по их синхронизации. Общие правила, которые при этом надо учиты- вать, перечислены ниже. Я Если все COM-серверы создаются в одном одпопоточпом апартаменте и этот апартамент создан в потоке, в котором работают клиенты этих серверов, пе требуется никаких мер по синхронизации, поскольку вся синхронизация будет автоматически обеспечена СОМ. Если серверы и клиенты работают в разных апартаментах, независимо от их типов следует синхронизировать доступ к глобальным переменным. Если несколько серверов работают в одном одпопоточпом апартаменте, мож- но пе заботиться о синхронизации доступа к полям объектов, реализующих эти серверы. И Если серверы работают в многопоточном апартаменте, следует синхронизи- ровать доступ как к глобальным переменным, так и к полям объектов, реали- зующих эти серверы. Следует немного подробнее остановиться па модели нейтральных потоков. Эта модель работает под управлением СОМ+; при отсутствии поддержки этой тех- нологии опа будет использоваться как модель разделенных потоков. Так же как и в модели свободных потоков, клиенты могут обращаться к методам из разных потоков. Отличие модели нейтральных потоков от модели свободных потоков заключается в следующем: метод сервера всегда вызывается в контексте вызываю- щего потока. Требования к объекту при его разработке такие же, как к объекту с моделью смешанных потоков. В следующем разделе рассматриваются вопросы синхронизации процессов и потоков в Windows. Эти вопросы пе являются уни- кальными для СОМ-приложепий и касаются любых многопоточных приложений. Синхронизация процессов Задача синхронизации встает при одновременном доступе нескольких процессов (или нескольких потоков одного процесса) к какому-либо ресурсу. Поскольку поток в Win32 может быть остановлен в любой, заранее ему неизвестный момент времени, возможна ситуация, когда один из потоков не успел завершить модифика- цию ресурса (например, отображенной па файл области памяти), по был останов- лен, а другой поток попытался обратиться к этому же ресурсу. В этот момент ресурс находится в несогласованном состоянии, и последствия обращения к нему могут быть самыми неожиданными — от порчи данных до нарушения защиты памяти. Главной идеей, положенной в основу синхронизации потоков в Win32, является использование объектов синхронизации и функций ожидания. Объекты могут находиться в одном из двух состояний — сигнальном (signaled) или несигналъном (not signaled). Функции ожидания блокируют выполнение потока до тех пор, пока заданный объект находится в песигпальпом состоянии. Таким образом, поток,
Синхронизация процессов 273 которому необходим эксклюзивный доступ к ресурсу, должен выставить какой-либо объект синхронизации в песигналыюе состояние, а но окончании — сбросить его в сигнальное. Остальные потоки должны перед доступом к этому ресурсу вы- звать функцию ожидания, которая позволит им дождаться освобождения ресурса. Рассмотрим, какие объекты и функции синхронизации предоставляет нам Win32 API. Функции синхронизации Функции синхронизации делятся па две основные категории — функции ожида- ния единственного объекта и функции ожидания одного из нескольких объектов. Функции ожидания единственного объекта Простейшей функцией ожидания является функция WaitForSingleObject: function WaitForSingleObject( hHandle: THandle: // идентификатор объекта dwMi11iseconds: DWORD // период ожидания ): DWORD: stdcal1; Эта функция ожидает перехода объекта hHandl е в сигнальное состояние в тече- ние dwMi 11 i seconds миллисекунд. Если в качестве параметра dwMi 11 i seconds пере- дать значение INFINITE, функция будет ждать в течение неограниченного времени. Если параметр dwMi 11 i seconds равен 0, то функция проверяет состояние объекта и немедленно возвращает управление. Функция возвращает одно из следующих значений: WAIT_ABANDONED — поток, владевший объектом, завершился, пе переведя объект в сигнальное состояние; М WAIT_OBJECT_O — объект перешел в сигнальное состояние; М WAIT_TIMEOUT — истек срок ожидания (обычно в этом случае генерируется ошибка либо функция вызывается в цикле до получения другого результата); М WAIT_FAILED — произошла ошибка, например неверное значение hHandle (более подробную информацию можно получить, вызвав функцию GetLastError). Следующий фрагмент кода запрещает действие Actionl до перехода объекта ObjectHandl е в сигнальное состояние. Например, таким образом можно дожидаться завершения процесса, передав в качестве параметра ObjectHandl е его идентифика- тор, полученный функцией CreateProcess. var Reason: DWORD: ErrorCode: DWORD; Actionl.Enabled := False: try repeat Appli cation.ProcessMessages:
274 Глава 6. Модели потоков и разработка многопоточных приложений Reason := WailForSingleObject(ObjectHandle, INFINITE): if Reason = WAIT_FAILED then begin ErrorCode ;= GetLastError; raise Exception.CreateFnit( 'Wait for object failed with error: W. [ErrorCode]); end; until Reason <> WAIT TIMEOUT; finally Actionl.Enabled ; = True; end; В случае когда требуется одновременно с ожиданием объекта перевести в < налыюе состояние другой объект, может использоваться следующая фупк! function SignalObjectAndWait( hObjectToSignal: THandle; 11 объект, который будет переведен в сигнальное состояние hObjectToWaitOn: THandle: // объект, который ожидает функция dwMilliseconds: DWORD; И период ожидания bAlertable: BOOL 11 параметр определяет, должна ли функция возвращать управление И в случае запроса на завершение операции ввода-вывода ): DWORD; stdcal1; Возвращаемые значения аналогичны соответствующим значениям фупки WaitForSingleObject. ВНИМАНИЕ ---------------------------------------------------------------------- В модуле Windows.pas эта функция ошибочно объявлена как возвращающая зна иие типа BOOL. Если вы намерены се использовать, объявите ее корректно или вып< пите приведение возвращенного значения к типу DWORD. Объект hObjectToSignal может быть семафором, событием или мьютексе (см. ниже подраздел «Объекты синхронизации» этого раздела). Параметр bAlertat определяет, будет ли прерываться ожидание объекта в случае, если операцио пая система запросит у потока окончание операции асинхронного ввода-выво, или асинхронного вызова процедуры. Более подробно это обсуждается далс Функции ожидания нескольких объектов Иногда требуется задержать выполнение потока до срабатывания одного ш всех сразу объектов из группы. Для решения подобной задачи служат функць WaitForMultipleObjects и MsgWaitForMul ti pl eObjects. Рассмотрим их более подробш type TWOHandleArray = array[0.,MAXIMUM_WAITJ)BJECTS - 1] of THandle: PWOHandleArray = *TWOHandleArray;
Синхронизация процессов 275 function Wai tForMulti pl eObjects( nCount: DWORD: // Количество объектов IpHandles: PWOHandleArray: 11 Адрес массива объектов bWaitAll: BOOL; // Необходимость ожидания всех 11 или любого из объектов dwMi11iseconds: DWORD И Период ожидания ): DWORD: stdcall; Эта функция возвращает одно из следующих значений: В число в диапазоне от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount - 1), причем: □ если параметр bWaitAll равен True, это число означает, что все объекты пе- решли в сигнальное состояние; □ если параметр bWaitAll равен False, то, вычтя из возвращенного значения WAIT_OBJECT_0, мы получим индекс объекта в массиве IpHandles; число в диапазоне от WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1), причем: □ если параметр bWaitAl 1 равен True, это означает, что все объекты перешли в сигнальное состояние, однако как минимум один из владевших ими по- токов завершился, не сделав объект сигнальным; □ если параметр bWaitAll равен False, то, вычтя из возвращенного значения WAIT_ABANDONED_0, мы получим индекс объекта в массиве IpHandles, при этом поток, владевший объектом, завершился, пе сделав его сигнальным; * WAIT_TIMEOUT — истек период ожидания; WAIT_FAILED — произошла ошибка. Например, в следующем фрагменте кода программа пытается модифициро- вать два различных ресурса, разделяемых между потоками: var Handles: array[O..l] of THandle; Reason: DWORD: Restindex: Integer: HandlesCO] := OpenMutexISYNCHRONIZE, FALSE. 'FirstResource'): Handles[l] := OpenMutexISYNCHRONIZE. FALSE. 'SecondResource'); 11 Ждем первый из объектов Reason : = WaitForMultiple0bjects(2. ^Handles, FALSE, INFINITE); case Reason of WAIT_FAILED: Rai seLastWin32Error; WAIT_OBJECT_O. WAIT_ABANDONED_O: begi n Modi fyFirstResource; RestIndex := 1: end: WAIT_DBJECT_O + 1. WAIT_ABANDONED_0 + 1:
276 Глава 6. Модели потоков и разработка многопоточных приложений begin Modi fySecondResource; Rest Index := 0; end; 11 значение WAIT_TIMEOUT возвращено быть не может end; // Теперь ожидаем освобождения следующего объекта if Wai 1ForSingleObject(HandlesERestIndex], INFINITE) = WAIT_FAILED then RaiseLastWin32Error; // Дождались, модифицируем оставшийся ресурс if Restindex = 0 then ModifyFirstResource else ModifySecondResource; Описанную выше технику можно применять, если вы точно знаете, что за- держка ожидания объекта окажется небольшой. В противном случае ваша про- грамма окажется «замороженной» и пе сможет даже перерисовать свое окно. Если период задержки может оказаться значительным, необходимо дать программе возможность реагировать па сообщения Windows. Выходом может служить ис- пользование функций с ограниченным периодом ожидания (и их повторный вызов, в случае возвращения значения WAIT_TIMEOUT). Можно также использовать следующую функцию: function MsgWaitForMultipl eObjects( nCount: DWORD; // Количество объектов синхронизации var pHandles; // Адрес массива объектов fWaitAll: BOOL; И Необходимость ожидания всех И или любого из объектов dwMi11iseconds, И Период ожидания dwWakeMask: DWORD // Тип события, прерывающего ожидание ): DWORD; stdcal1; Главное отличие этой функции от предыдущей — параметр dwWakeMask, кото- рый является комбинацией битовых флагов QS_XXX и задает типы сообщений, прерывающих ожидание функции независимо от состояния ожидаемых объек- тов. Например, маска QS_KEY позволяет прервать ожидание при появлении в оче- реди сообщения WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP или WM_SYSKEYDOWN, а маска QS_ PAINT — сообщения WM_PAINT. Полный список значений, допустимых для параметра dwWakeMask, имеется в документации по Windows SDK. Если для вызвавшего функцию потока в очереди появляются сообщения, соответствующие заданной маске, функция возвращает значение WAIT_DBJECT_O + nCount. Получив это значе- ние, ваша программа может обработать его и снова вызвать функцию ожидания. Рассмотрим пример с запуском внешнего приложения. Необходимо, чтобы на время его работы вызывающая программа не реагировала па ввод пользователя, однако ее окно должно продолжать перерисовываться. procedure TForml.ButtonlClick(Sender: TObject); var PI; TProcessInformation;
Синхронизация процессов 277 SI: TStartupInfo: Reason: DWORD: Msg: TMsg: begi n 11 Инициализируем структуру TstartupInfo FillCharCSI, SizeOf(SI). 0): Sl.cb := SizeOf(SI); // Запускаем внешнюю программу Win32Check(CreateProcess(nil. 'COMMAND.COM'. nil. nil, False. 0, nil, nil, SI. PI)); /1 -k-k-k-k-kkkkkk-kk-kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk 11 Попробуйте заменить нижеприведенный код на строку И H'aitForSIngleGbjectCPI.hProcess. INFINITE): И и посмотреть, как будет реагировать программа на И перемещение других окон над ее окном 11 k-k-kkkkkkkkkkkkkkkkkkkkkkkkk-kkkkkkkkkkkkkkkkk-kkkkkk repeat // Ожидаем завершения дочернего процесса или сообщения И перерисовки NM_PAINT Reason := MsgWaitForMultipleObjectsd. PI.hProcess. False. INFINITE, QS_PAINT): if Reason = WAIT_OBJECT_0 + 1 then begin // В очереди появилось сообщение WM_PAINT - Windows 11 требует обновить окно программы. // Удаляем сообщение из очереди PeekMessageCMsg, О, WM_PAINT. WM_PAINT, PM_REMOVE); // И перерисовываем наше окно Update: end; И Повторяем цикл, пока не завершится дочерний процесс until Reason = WAIT_OBJECT_0: 11 Удаляем из очереди накопившиеся там сообщения while PeekMessageCMsg. 0. 0, 0. PM_REMOVE) do: CloseHandle(PI.hProcess); CloseHandle(PI.hThread) end; Если в потоке, вызывающем функции ожидания явно (с помощью функции CreateWindow) или неявно (используя компонент TForm или технологию DDE или СОМ), создаются окна Windows, поток должен обрабатывать сообщения. Поскольку широковещательные сообщения посылаются всем окнам в операционной системе, поток, пе обрабатывающий сообщения, может вызвать взаимную блокировку (операционная система ждет, когда поток обработает сообщение, поток — когда операционная система или другие потоки освободят объект) и привести к зависа- нию Windows. Если в вашей программе имеются подобные фрагменты, необходимо использовать функцию MsgWai tForMul tipi eObjects или MsgWaitForMultipleObjectsEx и допускать прерывание ожидания для обработки сообщений. Алгоритм анало- гичен вышеприведенному примеру.
278 Глава 6. Модели потоков и разработка многопоточных приложений Прерывание ожидания по запросу Windows поддерживает асинхронные вызовы процедур (Asynchronous Procedure Calls, АРС). При создании каждого потока с ним ассоциируется очередь асинхрон- ных вызовов процедур (АРС queue). Операционная система (или приложение поль- зователя при помощи функции QueuellserAPC) может помещать в нее запросы па выполнение функций в контексте этого потока. Такие функции нельзя выполнить немедленно, поскольку поток может быть занят. Поэтому операционная система вызывает их, когда поток обращается к одной из следующих функций ожидания: function SleepEx( dwMi11iseconds: DWORD; 11 Период ожидания bAlertable: BOOL 11 Задает, должна ли функция возвращать И управление в случае запроса на // асинхронный вызов процедуры ): DWORD: stdcall: function WaitForSingleObjectEx( hHandle: THandle: 11 Идентификатор объекта dwMilliseconds: DWORD: 11 Период ожидания bAlertable: BOOL 11 Задает, должна ли функция возвращать II управление в случае запроса на И асинхронный вызов процедуры ): DWORD; stdcall: function WaitForMultipleObjectsEx( nCount: DWORD; 11 Количество объектов IpHandles: PWOHandleArray: 11 Адрес массива идентификаторов объектов bWaitAll: BOOL; 11 Необходимость ожидания всех или любого из объектов dwMi11iseconds: DWORD; 11 Период ожидания bAlertable; BOOL 11 Задает, должна ли функция возвращать И управление в случае запроса на И асинхронный вызов процедуры ): DWORD; Stdcall; function SignalObjectAndWait( hObjectToSignal: THandle: // Объект, который будет переведен 11 в сигнальное состояние
Синхронизация процессов 279 hObjectToWaitOn: THandle; 11 Объект, которого ожидает функция dwMi111 seconds; DWORD; 11 Период ожидания bAlertable: BOOL // Задает, должна ли функция возвращать И управление в случае запроса на // асинхронный вызов процедуры ): DWORD; stdcal1; function MsgWaitForMultipleObjectsEx( nCount: DWORD; // Количество объектов синхронизации var pHandles; // Адрес массива объектов fWaitAll: BOOL: 11 Необходимость ожидания всех или любого из объектов dwMi111 seconds. 11 Период ожидания dwWakeMask: DWORD. И Тип события, прерывающего ожидание dwFlags: DWORD // Дополнительные флаги ' ): DWORD; stdcal1: Если параметр bAlertable равен True (либо параметр dwFlags в функции MsgWaitForMultipleObjectsEx содержит значение MWMO_ALERTABLE), то при появлении в очереди АРС запроса па асинхронный вызов процедуры операционная система выполняет вызовы всех имеющихся в очереди процедур, после чего функция возвращает значение WAIT_IO_COMPLETION. Такой механизм позволяет реализовать, например, асинхронный ввод-вывод. Поток может инициировать фоновое выполнение одной пли нескольких опера- ций ввода-вывода функциями ReadFileEx или WriteFileEx, передав им адреса функций-обработчиков завершения операции. По завершении вызовы этих функ- ций будут поставлены в очередь асинхронного вызова процедур. В свою очередь, инициировавший операции поток, когда он будет готов обработать результаты, может, используя одну из вышеприведенных функций ожидания, позволить опера- ционной системе вызвать функции-обработчики. Поскольку очередь АРС реализо- вана па уровне ядра операционной системы, опа более эффективна, чем очередь сообщений, и позволяет реализовать гораздо более эффективный ввод-вывод. Объекты синхронизации Объектами синхронизации называются объекты Windows, идентификаторы ко- торых могут использоваться в функциях синхронизации. Они делятся па две группы — объекты, использующиеся только для синхронизации, и объекты, кото- рые используются в других целях, по могут вызывать срабатывание функций ожидания. К первой группе относятся события, мьютексы и семафоры.
280 Глава 6. Модели потоков и разработка многопоточных приложений События Объект событий (events) позволяет известить один или несколько ожидающих потоков о наступлении события. Существует два вида таких объектов. а Объекты, переводимые в несигналъное состояние «вручную». Такой объект, будучи установленным в сигнальное состояние, остается в нем до тех пор, пока не будет переключен явным вызовом функции ResetEvent. Ж Объекты, переводимые в несигналъное состояние автоматически. Такой объ- ект переключается в несигнальное состояние операционной системой, когда один из ожидающих его потоков завершается. Для создания объекта событий используется следующая функция: function CreateEvent( 1pEventAttri bates: PSecurityAttribates: // Адрес структуры TSecurityAttributes bManual Reset. // Указывает, будет ли объект переключаться И в несигнальное состояние И вручную (True) или автоматически (False) blnitiа 1 State: BOOL: // Задает начальное состояние. Если True - // объект в сигнальном состоянии IpName: PChar // Имя или nil. если имя не требуется ): THandle: stdcall: И Возвращает идентификатор созданного объекта Структура TSecurityAttributes описана следующим образом: TSecurityAttributes = record nLength: DWORD: // Структура должна инициализироваться 11 как SizeOf(TSecurityAttributes) IpSecurityDescriptor: Pointer: // Адрес дескриптора защиты. В windows 95 и 98 игнорируется blrtheritHartdlе: BOOL: // Указывает, могут пи дочерние процессы наследовать объект end; Если не требуются особые права доступа под Windows NT или наследование объекта дочерними процессами, в качестве параметра 1 pEventAttri butes можно передавать nil. В этом случае объект не может наследоваться дочерними процес- сами и ему задается дескриптор защиты «по умолчанию». Параметр 1 pName позволяет разделять объекты между процессами. Если 1 pName совпадает с именем уже существующего объекта событий, созданного теку- щим или любым другим процессом, функция не создает новый объект, а воз- вращает идентификатор уже существующего. При этом игнорируются пара- метры bManualReset, blnitialState и IpSecurityDescriptor. Проверить, был объект
Синхронизация процессов 281 создан заново или используется уже существующий объект, можно следующим образом: hEvent := CreateEvent(nil. TRUE, FALSE. 'EventName'): if hEvent = 0 then RaiseLastWin32Error; if GetLastError = ERROR_ALREADY_EXISTS then begin // Используем ранее созданный объект end: Если объект используется для синхронизации внутри одного процесса, его можно объявить как глобальную переменную и создавать без имени. Имя объекта не должно совпадать с именем любого из существующих объек- тов типа семафор, мьютекс, задание, таймер ожидания или файл, отображенный на память. В случае совпадения имен функция возвращает ошибку. Если известно, что объект событий уже создан, для получения доступа к нему можно вместо CreateEvent воспользоваться функцией: function OpenEvent( dwDesi redAccess: DWORD: 11 Задает права доступа к объекту MnheritHandle: BOOL: 11 Указывает, может ли объект наследоваться И дочерними процессами IpName: PChar И Имя объекта ): Thandle; stdcall: Функция возвращает идентификатор объекта либо 0 в случае ошибки. Пара- метр dwDesi redAccess может принимать одно из следующих значений: « EVENT_ALL_ACCESS — приложение получает полный доступ к объекту; « EVENT_MODIFY_STATE — приложение может изменять состояние объекта функциями SetEvent и ResetEvent; И SYNCHRONIZE (только для Windows NT) — приложение может использовать объект только в функциях ожидания. После получения идентификатора можно приступать к его использованию. Для этого имеются следующие функции: function SetEvent(hEvent: THandle): BOOL: stdcall: 11 Устанавливает объект в сигнальное состояние function ResetEvent(hEvent: THandle): BOOL; stdcall; // Устанавливает объект в несигнальное состояние function PulseEvent(hEvent: THandle): BOOL: stdcall 11 Устанавливает объект в сигнальное состояние, дает И отработать всем функциям ожидания, ожидающим этот объект, И а затем снова возвращает его в несигнальное состояние В Windows API события используются для выполнения операций асинхрон- ного ввода-вывода. В следующем примере показано, как приложение инициирует
282 Глава 6. Модели потоков и разработка многопоточных приложений запись одновременно в два файла, а затем ожидает завершения записи перед про- должением работы. При интенсивном вводе-выводе такой подход может обес- печить более высокую производительность, чем последовательная запись. var Events: array[O..l] of THandle; // Массив объектов синхронизации Overlapped: array[O..l] of TOverlapped; // Создаем объекты синхронизации Events[0] := CreateEventCnil. TRUE. FALSE, nil): Events[l] := CreateEvent(nil, TRUE. FALSE, nil): // Инициализируем структуры Toverlapped F111Char(Overlapped, SizeOf(Overlapped). 0); Overlapped[0].hEvent := Events[0J: Overlapped[l].hEvent := Events[l]: // Начинаем асинхронную запись в файлы WriteFi1е(hFirstFi1e, FirstBuffer. SizeOf(FirstBuffer). Fi rstFi1eWritten. @0verlapped[0]): WriteFile(hSecondF11e, SecondBuffer. SizeOf(SecondBuffer). SecondFi1eWri tten. @Overlapped[1]); // Ожидаем завершения записи в оба файла WaitForMultiple0bjects(2. OEvents. True. INFINITE): // Уничтожаем объекты синхронизации CloseHandlе(Events Е0]): CloseHandle(Events[l]): По завершении работы с объектом он должен быть уничтожен функцией CloseHandle. Delphi предоставляет класс TEvent, инкапсулирующий функциональность объ- екта событий. Класс расположен в модуле SyncObjs.pas и объявлен следующим образом: type TWaitResult = (wrSignaled. wrTimeout. wrAbandoned. wrError): TEvent = class(THandleObject) public constructor Create(EventAttributes: PSecurityAttributes: ManualReset. Initialstate: Boolean: const Name: String); function WaitFor(Timeout: DWORD): TWaitResult; procedure SetEvent: procedure ResetEvent; end;
Синхронизация процессов 283 Назначение методов очевидно из их названий. Использование этого класса по- зволяет не вдаваться в тонкости реализации вызываемых функций Windows API. Для простейших случаев объявлен еще один класс с упрощенным конструктором: type TSimpleEvent = class(TEvent) public constructor Create: end; constructor TSimpleEvent.Create: begin FHandle := CreateEvent(nil, True. False, nil); end; Прекрасный пример использования событий для синхронизации доступа можно найти в классе TMultiReadExclusi veWrlteSynchronizer из модуля Syslltils.pas. Исполь- зовать этот класс желательно в тех случаях, когда несколько потоков обращаются к общим переменным, причем обращение на чтение данных осуществляется значи- тельно чаще, чем на запись — типичная ситуация для данных, передаваемых через Интернет. Традиционные способы синхронизации доступа к данным (критические секции, мьютексы) неэффективны, поскольку, пока один из потоков не прочтет данные, все остальные находятся в состоянии ожидания, даже если им необходимы данные только для чтения. Класс TMultiReadExclusiveWriteSynchronizer решает проблему избыточной защищенности данных с помощью событий. Этот класс имеет четыре метода, которые вызываются попарно: BeginRead в паре с EndRead и BeginWrite в паре с EndWrlte. Поток, который хочет прочитать данные, обязан вызвать метод BeginRead и после окончания чтения данных — метод EndRead. Соответственно, при записи данных необходимо вызвать пару методов BeginWrite и EndWrite. При вызове метода BeginRead происходят следующие события: » если другие потоки не вызывали метод BeginRead или BeginWrite, то выставля- ется сигнал и можно обращаться к общим переменным для чтения; В если какие-либо из потоков выставили сигнал вызовом метода BeginRead, то данному потоку также разрешается чтение данных, при этом он может вытес- няться другими потоками, которые осуществляют чтение данных; В если какой-либо из потоков выставил сигнал вызовом метода BeginWrite, то поток будет находиться в состоянии ожидания вплоть до вызова метода EndWrite. При вызове метода BeginWrite происходят следующие события: в если другие потоки не вызывали метод BeginRead или BeginWrite, то выставля- ется сигнал и можно обращаться к переменным для их модификации; в если другие потоки вызывали метод Begi nRead или Begi nWri te, то выставляется сиг- нал и поток будет находиться в состоянии ожидания до тех пор, пока все пото- ки, которые читают данные, не вызовут метод EndRead или пока поток, который записывает данные, не вызовет метод EndWrite (если во время ожидания другие потоки вызовут метод BeginRead или BeginWrite, то они тоже будут ожидать).
284 Глава 6. Модели потоков и разработка многопоточных приложений Использование класса TMultiReadExclusi veWriteSynchronizer особенно эффек- тивно на многопроцессорных компьютерах. Мьютексы Мьютекс (mutex — mutually exclusive) — это объект синхронизации, который находится в сигнальном состоянии только тогда, когда он не принадлежит ни одному из процессов. Как только хотя бы один процесс запрашивает владение мьютексом, последний переходит в несигнальное состояние и остается в нем до тех пор, пока не будет освобожден владельцем. Такое поведение позволяет ис- пользовать мьютексы для синхронизации совместного доступа нескольких про- цессов к разделяемому ресурсу. Для создания мьютекса используется функция: function CreateMutexC 1pMutexAttri butes: PSecuri tyAttri butes: // Адрес структуры TSecuri tyAttributes blnitialOwner: BOOL: // Указывает, будет ли процесс владеть И мьютексом сразу после создания IpName: PChar // Имя мьютекса ): Thandle: stdcall: Функция возвращает либо идентификатор созданного объекта, либо 0. Если мьютекс с заданным именем уже был создан, возвращается его идентификатор. В этом случае функция GetLastError вернет код ошибки ERROR_ALREDY_EXISTS. Имя не должно совпадать с именем уже существующего объекта типа семафор, собы- тие, задание, таймер ожидания или файл, отображенный на память. Если неизвестно, существует ли уже мьютекс с таким именем, программа не должна запрашивать владение объектом при создании (то есть должна передать в параметре blnitialOwner значение False). Если мьютекс уже существует, приложение может получить его идентифика- тор с помощью функции OpenMutex: function OpenMutex( dwDes1redAcces s: DWORD; // Задает права доступа к объекту MnheritHandle: BOOL; // Задает, может ли объект наследоваться дочерними процессами IpName: PChar // Имя объекта ): Thandle: stdcall; Параметр dwDesi redAccess может принимать одно из следующих значений: Ж MUTEX_ALL_ACCESS — приложение получает полный доступ к объекту; И SYNCHRONIZE (только для Windows NT) — приложение может использовать объект только в функциях ожидания и в функции ReleaseMutex. Функция возвращает либо идентификатор мьютекса, установленного в сиг- нальное состояние, либо 0 в случае ошибки. Мьютекс переходит в сигнальное
Синхронизация процессов 285 состояние после срабатывания функции ожидания, в которую был передан его идентификатор. Для возвращения в несигнальное состояние служит функция: function ReleaseMutex(hMutex: THandle): BOOL; stdcall: Если несколько процессов обмениваются данными, например, через файл, отображенный на память, каждый из них для корректного доступа к общему ре- сурсу должен содержать следующий код: var Mutex: THandle: // При инициализации программы Mutex := CreateMutexInil. False, 'UniqueMutexName'): if Mutex = 0 then Rai seLastWIn32Error; // Доступ к ресурсу WaitForSingleObjectIMutex. INFINITE): try // Доступ к ресурсу, захват мьютекса гарантирует. // что остальные процессы, пытающиеся получить доступ, II будут остановлены на функции WaitForSingleObject finally // Работа с ресурсом окончена. И освобождаем его для остальных процессов ReleaseMutex(Mutex): end: // При завершении программы CloseHandlе(Mutex): Подобный код удобно инкапсулировать в класс, предназначенный для созда- ния защищенного ресурса. Мьютекс имеет свойства и методы для оперирования ресурсом, защищая их при помощи функций синхронизации. Разумеется, если работа с ресурсом может потребовать значительного времени, то необходимо либо использовать функцию MsgWaitForSingleObject, либо вызы- вать функцию Wai tForSingleObject в цикле с нулевым периодом ожидания, прове- ряя код возврата. В противном случае приложение окажется «замороженным». Всегда защищайте захват-освобождение объекта синхронизации при помощи блока try...finally, иначе ошибка во время работы с ресурсом приведет к блоки- рованию всех процессов, ожидающих его освобождения. Семафоры Семафор (semaphore) представляет собой счетчик, содержащий целое число в диа- пазоне от 0 до заданной при его создании максимальной величины. Счетчик уменьшается каждый раз, когда поток успешно завершает функцию ожидания,
286 Глава 6. Модели потоков и разработка многопоточных приложений использующую семафор, и увеличивается вызовом функции Rel easeSemaphore. При достижении семафором значения 0 он переходит в несигнальное состояние, при любых других значениях счетчика его состояние является сигнальным. Та- кое поведение позволяет использовать семафор в качестве ограничителя доступа к ресурсу, поддерживающего заранее заданное количество подключений. Для создания семафора служит функция CreateSemaphore: function CreateSemaphore( 1pSemaphoreAttri butes: PSecurityAttri butes: // Адрес структуры TSecurityAttributes UnitialCount, // Начальное значение счетчика 1 Maximumcount: Longint: // Максимальное значение счетчика 1pName: PChar // Имя объекта ): THandle: stdcall; Функция возвращает либо идентификатор созданного семафора, либо 0, если создать объект не удалось. Параметр 1 Maximumcount задает максимальное значение счетчика семафора, па- раметр 1 Initialcount — начальное значение в диапазоне от 0 до 1 Maximumcount. Па- раметр 1 pName задает имя семафора. Если в системе уже есть семафор с таким именем, то новый не создается, а возвращается идентификатор существующего семафора. В случае если семафор используется внутри одного процесса, можно создать его без имени, передав в качестве параметра 1 pName значение ni 1. Имя се- мафора не должно совпадать с именем уже существующего объекта типа собы- тие, мьютекс, задание, таймер ожидания или файл, отображенный на память. Идентификатор ранее созданного семафора может быть также получен с по- мощью функции: function OpenSemaphore( dwDes i redAccess: DWORD: // Задает права доступа к объекту blnheritHandlе: BOOL: // Задает, может ли объект наследоваться дочерними процессами 1pName: PChar // Имя объекта ): THandle: stdcall: Параметр dwDesi redAccess может принимать одно из следующих значений: И SEMAPHORE_ALL_ACCESS — поток получает все права на семафор; И SEMAPHORE_MODIFY_STATE — поток может увеличивать счетчик семафора функцией Releasesemaphore; М SYNCHRONIZE (только для Windows NT) — поток может использовать семафор в функциях ожидания.
Синхронизация процессов 287 Для увеличения счетчика семафора используется функция Releasesemaphore: function ReleaseSemaphore( hSemaphore: THandle: // Идентификатор семафора 1ReleaseCount: Longint; // Счетчик будет увеличен на эту величину IpPreviousCount: Pointer // Адрес 32-битной переменной с предыдущим значением счетчика ): BOOL; stdcall; Если значение счетчика после выполнения функции превысит заданный для него функцией CreateSemaphore максимум, то ReleaseSemaphore возвращает False и значение семафора не изменяется. В качестве параметра 1 pPrevi ousCount можно передать ni 1, если это значение нам не нужно. Рассмотрим пример приложения, запускающего на выполнение несколько за- даний в отдельных потоках (например, программу для фоновой загрузки файлов из Интернета). Если количество одновременно выполняющихся заданий будет слишком велико, то это приведет к неоправданной загрузке канала. Поэтому реа- лизуем потоки, в которых будут выполняться задания, таким образом, чтобы при превышении количеством потоков заданной величины очередной поток останав- ливался, ожидая завершения работы ранее запущенных потоков. unit LimitedThread; interface uses Classes: type TLimitedThread = class(TThread) procedure Execute; override; end: implementation uses Windows; const MAX_THREAD_COUNT = 10; var Semaphore; Thandle; procedure TLimi tedThread.Execute; begin // Уменьшаем счетчик семафора. 11 Если к этому моменту уже запущено // MAX_THREAD_COUNT потоков (счетчик равен 0) // и семафор находится в несигнальном состоянии.
288 Глава 6. Модели потоков и разработка многопоточных приложений // поток будет заморожен до завершения И одного из запущенных ранее потоков. WaitForSingleObject(Semaphore. INFINITE); // Здесь располагается код. отвечающий за функциональность И потока, например код загрузки файла // Поток завершил работу, увеличиваем // счетчик семафора и позволяем И начать обработку другим потокам ReleaseSemaphorelSemaphore, 1, nil); end; initialization // Создаем семафор при старте программы Semaphore := CreateSemaphorelnil. MAX_THREAD_COUNT, MAX_THREAD_COUNT. nil); Finalization // Уничтожаем семафор по завершении программы CloseHandle(Semaphore): end; Дополнительные механизмы синхронизации Критические секции Критические секции (critical sections) — это механизм, предназначенный для син- хронизации потоков внутри одного процесса. Как и мьютекс, критическая секция может в один момент времени принадлежать только одному потоку, однако она представляет собой более быстрый и эффективный механизм, чем мьютексы. Перед использованием критической секции необходимо инициализировать ее функцией InitializeCriticalSection: procedure Ini 11 а1i zeCгi ti са1 Secti on( var IpCriticalSection: TRTLCriticalSection ); Stdcal1; После создания объекта поток перед доступом к защищаемому ресурсу дол- жен вызвать функцию EnterCriticalSection: procedure EnterCriticalSectionI var IpCriticalSection: TRTLCriticalSection ); stdcal1; Если в этот момент ни один из потоков в процессе не владеет объектом, то поток становится владельцем критической секции и продолжает выполнение. Если секция уже захвачена другим потоком, то выполнение потока, вызвавшего функцию, приостанавливается до ее освобождения.
Синхронизация процессов 289 Поток, владеющий критической секцией, может повторно вызывать функцию EnterCriticalSectlon без блокирования своего исполнения. По завершении работы с защищаемым ресурсом поток должен вызвать функцию LeaveCriticalSect!on: procedure LeaveCriticalSection( var IpCriticalSection: TRTLCrlticalSection ): stdcall: Эта функция освобождает объект независимо от количества предыдущих вызо- вов потоком функции EnterCriticalSectlon. Если имеются другие потоки, ожидаю- щие освобождения секции, один из них становится ее владельцем и продолжает исполнение. Если поток завершился, не освободив критическую секцию, ее состоя- ние становится неопределенным, что может вызвать блокировку работы программы. Можно попытаться захватить объект без замораживания потока. Для этого служит функция TryEnterCriticalSection: function TryEnterCritica1 Sect!on( var IpCriticalSection: TRTLCrlticalSection ): BOOL: stdcall; Эта функция проверяет, захвачена ли секция в момент ее вызова. Если да — функция возвращает False, в противном случае — захватывает секцию и возвра- щает Т rue. По завершении работы с критической секцией она должна быть уничтожена вызовом функции DeleteCriticalSection: procedure DeleteCriticalSectionf var IpCriticalSection: TRTLCrlticalSection ): stdcal1: Рассмотрим пример приложения, осуществляющего в нескольких потоках за- грузку данных по сети. Глобальные переменные BytesSummary и TimeSummary хранят общее количество загруженных байтов и время загрузки. Эти переменные каждый поток обновляет по мере считывания данных. Для предотвращения конфликтов приложение должно защитить общий ресурс при помощи критической секции. var // Глобальные переменные С г iticalSecti on: TRTLCri tical Secti on: BytesSummary: Cardinal; TimeSummary: TDateTime: AverageSpeed: Float: // При инициализации приложения InitialIzeCriticalSectionICritical Section): BytesSummary := 0: TimeSummary := 0: AverageSpeed : = 0:
290 Глава 6. Модели потоков и разработка многопоточных приложений // В методе Execute потока, загружающего данные. repeat BytesRead := ReadDataBlockFromNetwork; EnterCri 11cal Section(CriticalSecti on); try BytesSummary := BytesSummary + BytesRead; TimeSummary := TimeSummary + (Now - ThreadStartTime); if TimeSummary > 0 then AverageSpeed ;= BytesSummary / (TimeSummary/24/60/60); finally LeaveCriticalSection(CriticalSection) end; until LoadComplete; // При завершении приложения DeleteCri ticalSection(Criti calSecti on): Delphi предоставляет класс, объявленный в модуле SyncObjs.pas, инкапсули- рующий функциональность критической секции; type TCriticalSection = class(TSynchroObject) public constructor Create: destructor Destroy; override; procedure Acquire; override: procedure Release: override: procedure Enter; procedure Leave; end: Методы Enter и Leave являются синонимами методов Acquire и Release соот- ветственно и добавлены для лучшей читаемости исходного кода; procedure TCriticalSection.Enter; begin Acqui re; end; procedure TCri ticalSection.Leave; begin Release; end; Защищенный доступ к переменным Часто возникает необходимость совершения операций над разделяемыми между потоками 32-разрядными переменными. Для упрощения решения этой задачи Win- dows API предоставляет функции защищенного доступа к переменным (interlocked variable access), не требующие дополнительных (и более сложных) механизмов синхронизации. Переменные в этих функциях должны быть выровнены по границе 32-разрядного слова. Применительно к Delphi это означает, что если переменная
Синхронизация процессов 291 объявлена внутри записи (record), то эта запись не должна быть упакованной (packed) и при ее объявлении должна быть активна директива компилятора {$А+}. Несоблюдение этого требования может привести к возникновению ошибок на многопроцессорных конфигурациях, что иллюстрируется приведенным ниже примером. type TPackedRecord = packed record A: Byte: B: Integer; end; // TPackedRecord.B нельзя использовать в функциях InterlockedXXX TNotPackedRecord = record A: Byte: B; Integer; end: {$A-} var Al: TNotPackedRecord: // Al.В нельзя использовать в функциях InterlockedXXX I; Integer // I можно использовать в функциях InterlockedXXX. так как // переменные в Delphi всегда выравниваются по границе слова. // независимо от состояния директивы компилятора $А {$Ан} var А2: TNotPackedRecord; // А2.В можно использовать в функциях InterlockedXXX Далее мы рассмотрим функции для защищенного доступа к разделяемым ме- жду потоками 32-разрядным переменным, предоставляемые Windows API. function Interlockedlncrementl var Addend: Integer ): Integer; stdcall; Функция увеличивает переменную Addend на 1. Возвращаемое значение зави- сит от операционной системы. Ж Windows 98, Windows NT 4.0 и выше — возвращается новое значение пере- менной Addend. В Windows 95, Windows NT 3.51: □ если после изменения Addend < 0, то возвращается отрицательное число, не обязательно равное Addend; □ если Addend = 0, возвращается 0;
292 Глава 6. Модели потоков и разработка многопоточных приложений □ если после изменения Addend > 0, то возвращается положительное число, не обязательно равное Addend. function InterlockedDecrementI var Addend: Integer ): Integer: stdcall: Функция уменьшает переменную Addend на 1. Возвращаемое значение анало- гично возвращаемому значению функции Interlockedlncrement. function InterlockedExchangeC var Target: Integer; Value: Integer ): Integer: stdcall: Функция записывает в переменную Target значение Value и возвращает пре- дыдущее значение переменной Target. Следующие функции для выполнения требуют Windows 98 или Windows NT 4.0 и выше. function Inter1ockedCompareExchange( var Destination: Pointer-, Exchange: Pointer; Comperand: Pointer ): Pointer: stdcall; Функция сравнивает значения Destination и Comperand. Если они совпадают, значение Exchange записывается в Destination. Функция возвращает начальное значение Destination. function InterlockedExchangeAddC Addend: PLongint; Value; Long!nt ): Long!nt: stdcall; Функция добавляет к переменной, на которую указывает Addend, значение Value и возвращает начальное значение Addend. Взаимная блокировка Рассмотрим следующий пример. Предположим, в главной форме приложения находится переменная, к которой осуществляется доступ из нескольких потоков как на чтение, так и на запись. Соответственно, для доступа к ней реализуем син- хронизацию с использованием семафора: TForml = class(TForm) Buttonl: TButton; Label 1: TLabel; procedure FormCreate(Sender: TObject): procedure ForrnDestroylSender: TObject);
Взаимная блокировка 293 private FValue:integer; procedure ThreadDone(Sender:TObject); function GetValue: integer; public property Value:integer read GetValue; end: var Forml: TForml; implementation var hSem; THandle = 0; {$R *.dfm} procedure TForml.ThreadDone(Sender; TObject): begin ShowMessagel'End'); end; function TForml.GetValue: integer; begin try WaitForSingleObject(hSem. INFINITE); Result:=FValue; finally ReleaseSemaphore(hSem, 1, nil); end; end; procedure TForml.FormCreatelSender; TObject); begin FValue:=50; hSem := CreateSemaphore(nil. 1. 1. nil); end: procedure TForml.FormDestroy(Sender; TObject); begin CloseHandle(hSem); end: В этом проекте обращение к свойству Value является потокозащищенным — до тех пор пока не будет выполнен метод GetValue, другие потоки не могут обра- титься к этой переменной.
294 Глава 6. Модели потоков и разработка многопоточных приложений Эта защита была бы лишней, если бы данная переменная была доступна только для чтения. Поэтому создадим класс-потомок TThread, где обращение к этой пе- ременной будет осуществляться как на чтение, так и на запись: type TMyThread-class(TThread) private FL:i nteger; procedure ReadValue: protected procedure Execute: override: end; procedure TMyThread.ReadValue; begin FL:=Forml.Value; try WaitForSingleObject(hSem, INFINITE): Forml.FValue:=FL+l; finally ReleaseSemaphore(hSem.l.nil); end; end: procedure TMyThread.Execute; begin ReadValue; repeat Synchronize!ReadValue); Sleep!100); until (FL>=100) or Terminated; end; В методе ReadValue происходит чтение переменной и затем записывается но- вое, измененное значение. Создадим экземпляр класса TMyThread при щелчке на кнопке: procedure TForml.ButtonlClick(Sender: TObject): begin with TMyThread.Create(True) do begin FreeOnTerminate := True: OnTerminate := ThreadDone; Resume: end; end: Данное приложение работает вполне корректно: при щелчке на кнопке в тек- стовом поле выводятся промежуточные значения и после окончания расчетов приходит сообщение о завершении потока. Теперь немного изменим код проце-
Взаимная блокировка 295 дуры ТМуThread.ReadValue, а именно — обратимся свойству TForml.GetValue после вызова метода WaitForSingleObject: procedure TMyThread.ReadValue; begin try WaitForS1ngle0bject(hSem, INFINITE): FL:=Forml.Value; Forml.FValue;=FL+l; finally ReleaseSemaphore(hSem.1.ni1): end; end: При щелчке на кнопке в этом приложении вывод промежуточной информа- ции не осуществляется, и сколько бы времени мы ни ждали, сообщение о завер- шении работы потока не появится на экране. То есть в данном приложении поток никогда не завершит свою работу. Для понимания причины необходимо вспомнить работу метода WaitForSingleObject: он ожидает сигнального состояния семафора, и при получении сигнала или по йстечении времени ожидания начинает выполняться последующий код, и одновременно семафор переходит в несиг- нальное состояние. Время ожидания в данном проекте установлено бесконечно большим. Сначала семафор находится в сигнальном состоянии, вызов метода WaitForSingleObject из TMyThread.ReadValue переводит его в несигнальное состоя- ние, и код продолжает выполняться. После этого происходит обращение к методу WaitForSingleObject из кода главного потока в методе GetValue для чтения зна- чения FValue, при этом фоновый поток переводится в состояние ожидания. Но в методе GetValue;WaitForSingleObject семафор не переводится в сигнальное состоя- ние, поэтому главный поток также переходит в режим ожидания. Таким обра- зом, для дальнейшей работы метода TMyThread. Read Value необходимо завершение метода TForml.GetValue, а для работы метода TForml.GetValue — завершение метода TMyThread.GetValue и перехода семафора в сигнальное состояние. Такое бесконеч- ное ожидание называется взаимной блокировкой (deadlock). На первый взгляд, данный пример может показаться надуманным — в самом деле, кто же после вызова метода WaitForSingleObject будет вызывать его повторно, не переводя семафор в сигнальное состояние? Однако надуманность кажущаяся — повторное обращение может осуществляться в результате долгой цепочки вызо- вов, при этом программист может не догадываться о существовании блокировок. Ниже даны некоторые конкретные примеры. К В проектах, реализованных с помощью Delphi 2, при вызове метода WaitFor происходила взаимная блокировка, если из потока вызывались методы с по- мощью метода Synchronize. Я В DataSnap-приложениях при попытке изменить содержимое визуальных элементов управления, описанных на уровне Windows API (текстового поля, обычного и комбинированного списков, заголовка формы), из конструктора класса — потомка TRemoteDataModul е происходит взаимная блокировка. Об этом будет подробно рассказано в главе 12.
296 Глава 6. Модели потоков и разработка многопоточных приложений Потокозащищенные классы Delphi Многие классы Delphi имеют методы, которые позволяют синхронизировать дос- туп к переменным классов при обращении к ним из различных потоков. Класс TCanvas — имеет методы Lock и Unlock. После вызова метода Lock рисова- ние на полотне (canvas) возможно только из потока, откуда осуществлен вызов этой команды, а остальные потоки находятся в состоянии ожидания. Пример, который обсуждался в начале этой главы — вывод графики на экран при одно- временной сортировке случайного массива в разных потоках, — можно перепи- сать с использованием этих методов: procedure TSortThread.DoV1sualSwap; begin with FBox do try Canvas.Lock; Canvas.Pen.Col or := clBtnFace: PaintLine(Canvas. FI, FA): PaintLine(Canvas, FJ, FB); Canvas.Pen.Col or := cl Red; PaintLine(Canvas. FI, FB): PaintLinelCanvas. FJ. FA); finally Canvas.Unlock: end: end: Переписанный код можно вызывать без использования метода Synchronize, причем он будет работать быстрее. Графические объекты — потомки TGraphicsObject, которые инкапсулируют функциональность графических объектов Windows — кисть (TBrush), перо (ТРеп) и шрифт (TFont), также имеют методы Lock и Unlock. Использование и назначение их такое же, как и в случае класса TCanvas. Методами Lock и Unlock, которые используются для защиты переменных при доступе к ним из разных потоков, также обладают следующие классы Delphi: Ж TInterfaceLlst — хранит список интерфейсов; И TRemoteDataModule — используется в серверной части при создании DataSnap- приложений. Методы Lock и Unlock вызываются автоматически при работе сервера в модели разделенных потоков. Если используется модель свободных потоков, то программисту следует в явном виде вызывать эти методы. Класс TThreadList наиболее часто используется при создании многопоточных приложений. Он хранит массив указателей, причем берет у операционной систе- мы столько ресурсов, сколько надо для хранения массива данной размерности. Путем приведения типов в нем можно хранить и целочисленные переменные (integer), и переменные с плавающей точкой (single). Перекрыв деструктор класса, в классе-потомке можно хранить список объектов. Эти разнообразные возмож-
Заключение 297 ности и делают данный класс часто применимым. Типичный пример работы с классом TThreadLi st: var UserList:TThreadList = nil; procedure TTest.AfterConstruction; var L:TList: begin inherited: try L := UserList.LockList: L.Add(Self): finally UserList.UnlockList: end: end: initialization TComponentFactory.Create(ComServer. TTest. Class_Test. ciMultiInstance. tmFree); UserList := TThreadList.Create: finalization UserList.Free: end. В глобальной переменной UserLi st сохраняются ссылки на клиентов, которые обращаются к серверу за данными. Вызов метода LockList возвращает ссылку на класс TList, с которым манипулируют как с обычным списком. После вызова ме- тода Uni ockLi st использовать полученную ссылку на список запрещается. Каждому вызову метода Lock должен соответствовать вызов метода Uni ock. По- этому во всех описанных выше классах методы блокировки доступа (Lock, LockList) и снятия блокировки (Unlock, UniockList) следует помещать в защищенный блок try...finally...end. Несоблюдение этого правила приведет к тому, что к данному экземпляру класса нельзя будет обратиться из других потоков, кроме потока, вызвавшего метод Lock. Поскольку все описанные выше блокировки реализо- ваны через критические секции, то поток, который вызвал метод Lock, может по- вторно его вызвать и выполнить код после этого вызова, даже если метод Uni ock после первого вызова выполнен не был. Это затрудняет поиск ошибок, которые возникают при невыполненном методе Uni ock. Заключение Многозадачная и многопоточная среда Win32 предоставляет широкие возможно- сти для написания высокоэффективных приложений. Однако написание прило- жений, использующих многопоточность и взаимодействующих друг с другом,
298 Глава 6. Модели потоков и разработка многопоточных приложений при неаккуратном программировании может привести к их неверной работе, не- оправданной загрузке и даже блокировке всей системы. Во избежание этого сле- дуйте нижеприведенным рекомендациям. Если приложения или потоки одного процесса изменяют общий ресурс, за- щищайте доступ к нему при помощи критических секций или мьютексов. й Если доступ осуществляется только на чтение, защищать ресурс не обяза- тельно. Ш Хотя критические секции более эффективны, они применимы только внутри одного процесса, поэтому для синхронизации между процессами следует при- менять мьютексы. Ш Используйте семафоры для ограничения количества обращений к одному ре- сурсу. к Используйте объекты событий для информирования потока о наступлении какого-либо события. « Если разделяемый ресурс — 32-битная переменная, то для синхронизации доступа к нему можно использовать функции, обеспечивающие совместный доступ к переменным. ® Многие объекты Win32 позволяют организовать эффективное слежение за своим состоянием при помощи функций ожидания. Используйте функции ожидания, поскольку они предлагают наиболее эффективный с точки зрения расхода системных ресурсов метод. Ш Если ваш поток создает (даже неявно, при помощи функции Coinitialize или функций DDE) окна, он должен обрабатывать сообщения. Не используйте в таком потоке функций, не позволяющих прервать ожидание по приходу со- общения с большим или неограниченным периодом ожидания, используйте функции MsgWaitForXXX. Закончив изучение потоков, мы можем перейти к детальному рассмотрению внутрипроцессных СОМ-серверов — именно при их создании важно правильно организовать работу с потоками. Этому вопросу будет посвящена следующая глава.
ГЛАВА 7 Создание внутрипроцессных серверов автоматизации Глава 3 была посвящена созданию сервера автоматизации, выполненного в виде исполняемого файла. Однако нередко сервер автоматизации размещается в дина- мически загружаемой библиотеке (DLL). При обращении к такому серверу про- исходит загрузка библиотеки (если ранее она не была загружена) и запуск вызы- ваемой функции. В этой главе сначала мы рассмотрим создание традиционных библиотек, не содержащих COM-объектов, а затем обсудим особенности реали- зации сервера автоматизации в виде DLL. Создание и использование динамически загружаемых библиотек В данном разделе речь пойдет о традиционных библиотеках, которые не содержат COM-объектов. Большая часть из сказанного далее верна и для DLL с СОМ- объектами, отличия же будут специально выделяться. Преимущества реализации кода в DLL Динамически загружаемые библиотеки являются, пожалуй, одним из наиболее мощных средств создания приложений в Windows. По структуре данных DLL на- поминает исполняемый файл (ЕХЕ-файл), но в отличие от исполняемого файла код, содержащийся в DLL, не может выполняться самостоятельно. Зато библио- тека (так же как и ЕХЕ-файл) может быть загружена в память компьютера, и ра- ботающие приложения могут вызвать экспонируемые в DLL функции. На основе DLL создаются элементы управления ActiveX. Преимущества использования DLL следующие. Прежде всего, функции, опи- санные в DLL, могут одновременно обслуживать несколько приложений. При этом сами функции хранятся в памяти в виде единственной копии. Если вызы- ваемый код достаточно велик и имеется несколько приложений, которые вызы- вают данный код, достигается существенная экономия системных ресурсов. Второе преимущество — возможность хранения общих ресурсов. Опять же, если несколько приложений работают с одними и теми же ресурсами (например, с большими растровыми изображениями — BMP-файлами), то достаточно со- хранить в DLL единственную копию этих ресурсов.
300 Глава 7. Создание внутрипроцессных серверов автоматизации Третье преимущество — поддержка новых версий приложений. Если програм- мистом были сделаны какие-либо изменения в реализациях функций, опреде- ленных в DLL, то конечному пользователю достаточно передать новую версию DLL — ЕХЕ-файл можно оставить прежним. Это особенно актуально сейчас, когда версии многих приложений можно изменять через Интернет, и важно миними- зировать объем данных, передаваемых при таком обновлении. Естественно, если часть кода реализована в виде DLL, то при загрузке с сервера только этой биб- лиотеки сетевой трафик окажется ниже, чем при загрузке приложения целиком. Четвертое преимущество заключается в возможности использования различ- ных языков программирования для создания EXE- и DLL-файлов. Например, ЕХЕ-файл может компилироваться из кода, написанного на Delphi, а DLL-файл, который им используется, из кода, написанного на Microsoft Visual C++. Если приложение использует несколько библиотек, то они могут быть созданы на различных языках программирования. Это значительно упрощает перенос кода в другие приложения. И, наконец, библиотеки можно загружать в память только тогда, когда они требуются для выполнения приложений — такое их применение называется дина- мической загрузкой. При этом опять же достигается экономия системных ресур- сов. Так, например, в DLL можно реализовать диалоговое окно для изменения каких-либо параметров приложения. Пользователь может месяцами не обра- щаться к данному диалоговому окну, и при этом библиотека, в которой оно реа- лизовано, не загружается в память и не потребляет системных ресурсов. Только в тот момент, когда пользователь обращается к данному диалоговому окну, про- исходит загрузка DLL в память, но после окончания использования диалогового окна эта память освобождается. Благодаря динамической загрузке можно соз- дать динамический пользовательский интерфейс — в этом случае соответствую- щие пункты меню появляются при наличии данной библиотеки и исчезают при ее отсутствии. Такой интерфейс удобен при поставке приложений, в которых пользователь за отдельную плату может заказать дополнительные функциональ- ные возможности. Создание простейшей библиотеки Delphi имеет мастер для создания DLL, который вызывается командой File ► New ► Other и последующим выбором значка DLL wizard на странице New репози- тария объектов. При этом возникает заготовка для реализации DLL: library FirstLib: uses Sysllti 1 s. Classes: begin end. В приведенном выше коде отсутствует Текстовый комментарий, который генерируется мастером. Заготовка отличается от заготовки для создания кода
Создание и использование динамически загружаемых библиотек 301 ЕХЕ-файла тем, что вместо служебного слова program используется служебное слово library. Кроме того, в коде отсутствуют обращение к методам объекта TAppl icatlon (хотя экземпляр этого объекта и создается в библиотеке!) и модуль реализации главной формы. Создадим в полученной заготовке функцию, которая будет имитировать выполнение каких-либо вычислений: function AddOne(N: Integer): Integer: stdcall: export: begin Result := N + 1: end: Как видно, код реализации функции AddOne включает две директивы stdcal 1 и export, которые в реализации функций при создании приложения обычно отсутствуют. Директива stdcall связана с соглашениями о вызове функций. Рассмотрим их здесь подробнее. Когда в приложении осуществляется вызов функции, ее параметры (так же, как и локальные переменные) помещаются в стек. Стек представляет собой заре- зервированное место в памяти компьютера. Он имеет указатель текущей пози- ции, который при старте приложения устанавливается на начало стека. При вызове функции в стек помещаются все ее локальные переменные и параметры, при этом указатель текущей позиции стека смещается вправо на размер помещае- мых в него данных. Если функция, в свою очередь, вызывает другую функцию, то локальные переменные второй функции добавляются в стек, так же как и список параметров. После окончания работы второй функции происходит освобождение области памяти в стеке — для этого указатель текущей позиции стека смещается влево. И наконец, после окончания работы первой функции указатель текущей позиции стека смещается в первоначальное положение. Сказанное иллюстри- рует рис. 7.1. шш! — свободная память — занятая память Рис. 7.1. Изменение состояния стека при вызове функций Ясно, что при нормальной работе приложения после окончания выполнения цепочки функций указатель текущей позиции стека должен вернуться в пер- воначальное состояние, то есть должна быть выполнена очистка стека (stack
302 Глава 7. Создание внутрипроцессных серверов автоматизации cleanup). Если же указатель не возвращается в первоначальное состояние, то происходит крах стека (stack crash), который не надо путать с очисткой стека. При этом приложение прекращает свою работу (никакие ловушки исключений при этом не помогают), и если оно выполняется под управлением Windows 95 (или Windows 98), чаще всего требуется перезагрузка операционной системы. Понятно, что возвращение указателя стека в первоначальное состояние должно происходить по окончании работы функции. Но при этом существует две воз- можности — возвращение указателя на место может производить как вызывае- мая функция при окончании работы, так и вызывающий ее код после окончания работы вызываемой функции. В принципе, в разных языках программирования реализуются обе возможности — очищать стек может как вызванная функция, так и вызывающий код. Поскольку модуль пишется на одном языке программи- рования, то эти проблемы скрыты от программиста — очистка стека производится согласно специфическим для данного языка правилам. Но если используются различные модули, код для которых реализован на различных языках программи- рования, то возникают проблемы. Например, в C++ стек очищается в функ- ции, которая вызвала вторую функцию, после окончания работы второй функции. В Delphi же стек очищается в той же самой функции, в которой он используется, перед окончанием ее работы. Если ЕХЕ-модуль, созданный на языке C++, вызы- вает функцию из библиотеки, созданной в Delphi, то перед окончанием работы этой функции в DLL будет очищен стек. После этого управление передается мо- дулю, реализованному на C++, который также попытается очистить стек, и такое действие приведет к краху стека. Кроме этих возможностей существует проблема, связанная с очередностью, с которой параметры функции помещаются в стек. Предположим, имеется функ- ция, которая для работы использует два параметра: procedure DoSomething(N: Integer: D: TDateTime): Альтернатива заключается в том, что в начале в стек может быть помещена константа N, а затем D (слева направо), или вначале помещается константа D, а за- тем N (справа налево). Кроме того, некоторые языки программирования (в част- ности, Delphi) часть параметров функции вообще не помещают в стек, а передают их через регистры процессора. Опять же, в разных языках программирования параметры в стек могут помещаться как слева направо, так и справа налево. При этом если они были помещены слева направо, а вызываемая функция будет читать их справа налево, то результат чтения параметров окажется некоррект- ным — в данном примере в качестве значения константы N вызываемая функция будет считывать значение правой половины константы D, а константу D она сфор- мирует из константы N и левой половины D. Поэтому в языках программирования высокого уровня можно объявить, кто будет очищать стек и в какой очередности параметры функции будут помещаться в стек. Такое объявление называется соглашением о вызовах (calling convention). Имеется ряд зарезервированных слов, которые помещаются после заголовков функций (табл. 7.1).
Создание и использование динамически загружаемых библиотек 303 Таблица 7.1. Соглашения о вызовах в Delphi Директива Порядок следования параметров Очистка стека Регистры register Слева направо Вызываемая функция Используются pascal Слева направо Вызываемая функция Не используются cdecl Справа налево Вызывающий код Не используются stdcal1 Справа налево Вызываемая функция Не используются safecal1 Справа налево Вызываемая функция Не используются Для функций, экспонируемых в DLL, рекомендуется (но не обязательно) реа- лизовывать то соглашение о вызовах, которое используется в Windows API. Для 32-разрядных приложений функции Windows API реализованы таким образом, что параметры в стек помещаются справа налево и стек очищает вызываемая функция, при этом регистры процессора для передачи данных не задействуются. Этим условиям удовлетворяет директива stdcal 1, которая в примере, описанном выше, помещается после заголовка функции AddOne. Если после заголовка функ- ции не указана директива, описывающая соглашение о вызовах, по умолчанию в Delphi принимается соглашение register. Второе служебное слово в заголовке функции — export — информирует ком- пилятор, что код данной функции должен быть создан таким образом, чтобы его можно было вызывать из других модулей. Эта директива требуется при реализа- ции DLL в Delphi 3; в более поздних версиях Delphi ее можно опускать. Однако написанного выше кода еще недостаточно для вызова функции AddOne из другого модуля. Одна библиотека может предоставлять несколько функций внешнему модулю. Для того чтобы внешний модуль мог выбрать конкретную функцию, в DLL обязательно должна присутствовать специальная секция, вводи- мая ключевым словом exports (не путать с директивой export). Для нашего при- мера эту секцию можно объявить следующим образом: exports AddOne index 1 name 'CalculateSum': Для экспонирования функции в секции exports просто приводится его название (AddOne), после которого следует либо служебное слово index с целочисленным идентификатором после него (идентификатор должен быть больше нуля), либо служебное слово name с текстовым идентификатором, либо оба вместе, как в данном случае. Внешний модуль может обращаться к конкретной функции как по индексу, так и по имени. Как это делается — будет рассказано в следующем разделе. На данном этапе изложения материала следует отметить, что название функции AddOne нигде и никогда не будет видно во внешних модулях — будет использо- ваться либо целочисленная константа 1, либо имя CalculateSum. Сразу же следует отметить, что имя чувствительно к регистру букв — функция не будет найдена, если использовать, например, такие имена, как calculatesum или CALCULATESUM. Ин- дексы, если они объявляются в секции exports, обязательно должны начинаться
304 Глава 7. Создание внутрипроцессных серверов автоматизации с 1 и принимать все последовательные целочисленные значения (2, 3, 4...). Нельзя опускать какое-либо число в этой последовательности натуральных чисел, иначе могут возникнуть ошибки при обращении к функции по ее индексу. Эта ошибка проявляется в Windows 95 0SR-2 из-за некорректной реализации этой операци- онной системы. Для исправления ошибки компания Microsoft объявила о том, что библиотека обязательно должна экспортироваться по имени. Поэтому во вновь создаваемых библиотеках необходимо объявлять имя функции в секции exports, при этом индексы объявлять не следует (или, по крайней мере, не следует использовать их для выяснения адреса функции). И, наконец, следует рассмотреть отличия в реализации обычных библиотек и библиотек, содержащих COM-объекты. В секции exports библиотек, содержа- щих COM-объекты, имеется четыре функции, о которых будет сказано ниже. Конечно, можно добавлять и другие функции, но для их вызова не требуется СОМ. Для экспорта других функций библиотеки СОМ используют интерфейсы. Все функции COM DLL (как объявленные в секции exports, так и экспонируемые как методы COM-интерфейсов) обязательно должны иметь директивы вызова либо stdcall, либо safecall. Эти директивы будут обсуждаться ниже. Статическая и динамическая загрузка DLL Модуль может вызывать функции другого модуля, тот, в свою очередь, функции следующего и т. д. Например, приложение вызывает библиотеку, а эта библиотека вызывает функции другой библиотеки — так можно формировать длинные це- почки вызовов. Для вызова функции из другого модуля необходимо сначала за- грузить ее в память, а затем определить адрес функции. Существует два способа загрузки и определения адреса функции — статический и динамический. При статической загрузке для вызова другого модуля следует в какой-либо из секций описать функцию, вызываемую из DLL, одним из следующих спо- собов: function Addl(К: Integer): Integer: stdcall: external 'FirstLib.dll' name 'CalculateSum'; function Addl(K: Integer): Integer; stdcall; external 'FirstLib.dll’ index 1; Для тестирования необходимо описать в приложении внешнюю функцию одним из вышеупомянутых способов и создать, например, обработчик события OnClick кнопки, помещенной на форму вместе с компонентом TEdit: procedure TForml.ButtonlClick(Sender: TObject): var N: Integer: begi n N := StrToInt(Editl.Text): N := Addl(N); Editl.Text := IntToStr(N); end;
Создание и использование динамически загружаемых библиотек 305 При щелчке на кнопке будет вызываться функция из DLL. Обратите внимание на изменение имени функции: из обработчика события OnClick вызывается функ- ция с именем Addl. Эта функция экспонируется в DLL под именем CalculateSum. В реализации DLL она имеет название AddOne. При таком определении функции библиотека будет загружена немедленно после старта приложения и выгружена вместе с его завершением. В приведенном выше примере следует обратить внимание на то, что после имени библиотеки указано ее расширение (FirstLib.dll). Такая конструкция необходима для загрузки библиотеки в Windows NT, Windows 2000 и Windows ХР — без расширения *.dll файл найден не будет. Для работы приложения под управлением Windows 95/98 указывать расширение не обязательно. При поиске DLL для загрузки первоначально определяется, была ли данная библиотека уже загружена в память другим модулем. Если была — извлекается адрес вызываемой функции и затем передается приложению. Если же нет — опе- рационная система начинает ее поиск на диске. При этом если в имени DLL путь не указан в явном виде, то операционная система ищет библиотеку в ка- талоге модуля, пытающегося загрузить DLL. Если не находит — то продолжает поиск в каталогах WINDOWS и WINDOWS\SYSTEM (или WINNT, WINNT\SYSTEM, WINN T\SYSTEM32). После этого происходит поиск в каталогах, определенных в переменной среды Path. Если операционная система обнаруживает библиотеку с заданным именем, она загружает эту библиотеку и приложение стартует. Если же нет — возникает исключение и приложение прекращает свою работу. Прило- жение также прекращает свою работу, если не найдена функция с данным име- нем (или индексом, если она импортируется по индексу). Динамическая загрузка DLL выполняется только тогда, когда она требуется. Кроме того, даже если библиотека или вызываемая функция из этой библиотеки найдена не будет, эту ситуацию можно проанализировать и все равно запустить приложение. Конечно, при этом следует информировать пользователя о невоз- можности вызвать функцию из DLL, например, сделав недоступным пункт ме- ню, с помощью которого пользователь обращается к данной функции. Пример динамической загрузки DLL выглядит следующим образом: type TAddFunction = functionCK: Integer): Integer; stdcall: procedure TForml.Button2Click(Sender: TObject); var Addl: TAddFunction; HLib: THandle: N: Integer: begin HLib := 0; try HLib:=LoadLibrary('FirstLib.dll’): if HLib <> 0 then begin Addl := GetProcAddress(HLib. 'CalculateSum'):
306 Глава 7. Создание внутрипроцессных серверов автоматизации if Assigned(Addl) then begin N := StrToInt(Editl.Text): N : = Addl(N): Editl.Text := IntToStr(N): end else ShowMessageC'Method with name'+ ' CalculateSum was not found'): end else ShowMessageC'Can not load library’ + ' FirstLib.dll'): finally if HLib <> 0 then FreeLibrary(HLib): end: end: Первоначально определяется новый процедурный тип — например, TAddFunction, который имеет тот же самый список параметров и то же самое соглашение о вы- зовах, что и функция в DLL. Delphi в процессе компиляции приложения никак не может проверить соответствие процедурного типа функции, вызываемой из DLL. Проверка может быть осуществлена только во время выполнения — при несоответствии формальных параметров или неверно указанного соглашения о вызовах происходит крах стека, и программист это очень быстро обнаружит. Далее в коде приложения вызывается функция LoadLibrary, которая в каче- стве параметра использует имя библиотеки. После успешной отработки этой функции и загрузки библиотеки в память дескриптор загруженной библиотеки помещается в переменную HLib. Если же не удается найти (или загрузить) библио- теку, то в эту же переменную помещается код ошибки. Для определения факта загрузки библиотеки переменную HLib следует сравнить с нулем. Если библиотека была успешно загружена, то делается попытка найти адрес функции в памяти компьютера путем вызова функции Windows API GetProcAddress, возвращающей адрес функции, имя которой указано во втором параметре GetProcAddress. Если же функция не найдена, возвращается значение nil. Соответственно, вызов функции Addl осуществляется, только если она была успешно найдена. Далее, поскольку при загрузке библиотеки были заняты сис- темные ресурсы, их необходимо вновь вернуть системе, выгрузив библиотеку из памяти. Для этого вызывается функция FreeLibrary. При загрузке DLL произво- дится подсчет ссылок — а именно, при каждом успешном обращении к функции LoadLibrary в DLL счетчик ссылок увеличивается на единицу. При каждом вызове функции FreeLibrary счетчик ссылок уменьшается на единицу. Как только счет- чик ссылок станет равным нулю, библиотека может быть выгружена из памяти компьютера. Поэтому каждому успешному вызову LoadLibrary должно соответство- вать обращение к FreeLibrary — иначе DLL не выгрузится из памяти компьютера до окончания работы приложения. Поэтому перечисленные функции помещены в защищенный блок try...finally...end — это гарантирует вызов функции FreeLibrary, если происходит исключение. Функция GetProcAddress может также использовать индексы для загрузки DLL. Для примера, рассмотренного выше, в котором функция AddOne экспонируется
Создание и использование динамически загружаемых библиотек 307 с индексом 1, применение индексов в функции GetProcAddress выглядит следую- щим образом: Addl := GetProcAddress(HLib, PChar(l)); Однако при наличии индексов следует соблюдать осторожность. Необходимо, чтобы все функции в DLL были проиндексированы со значениями индексов от единицы до N (N — число функций, объявленных в секции exports). Если ка- кой-либо индекс опускается (в этом случае максимальное значение индекса бу- дет больше числа функций), то GetProcAddress возвращает ненулевой адрес для несуществующего индекса. Очевидно, что этот адрес является недействительным и при попытке обращения к нему генерируется исключение. По-видимому, по причине некорректной работы функции GetProcAddress компания Microsoft запре- тила использовать индексы для импорта функций из DLL. Теперь следует рассмотреть, каким образом загружаемая библиотека размеща- ется в памяти компьютера. При загрузке DLL осуществляется резервирование памяти, необходимое для хранения кода функций. Кроме того, резервируется место для всех глобальных переменных и выполняются секции инициализации в модулях DLL. Если другой процесс также пытается загрузить DLL, то вновь происходит резервирование памяти для хранения глобальных переменных. Однако копирование функций DLL не осуществляется. Другими словами, одна копия функции в памяти обслуживает несколько приложений. Глобальные перемен- ные являются уникальными для каждого приложения, и если одно приложение изменит их значение путем вызова какой-нибудь функции, то другое приложе- ние этого не заметит. Библиотеки COM DLL загружаются только динамически, при обращении клиента к интерфейсу, который находится в данной библиотеке. Для того чтобы библиотека СОМ загружалась сразу же после старта приложения и выгружалась после его окончания (аналог статической загрузки), требуется написание кода, в частности кода вызова функции LockServer для фабрики классов или использо- вания функции CoLoadLlbrary (она была рассмотрена в главе 1). Обмен данными с DLL Библиотека DLL имеет общее адресное пространство с приложением, из кото- рого вызываются ее функции. Это означает, что указатель на какой-либо объект в памяти библиотеки является легальным внутри приложения (и наоборот). Это позволяет передать, например, адрес функции или адрес данных, чего при взаи- модействии двух приложений нельзя сделать без маршалинга. Однако передача данных между двумя модулями существенно отличается от передачи данных ме- жду двумя функциями одного модуля — в различных модулях имеются разные диспетчеры памяти (memory manager). Это означает, что если в каком-то модуле (например, в DLL) была вызвана функция GetMem, то освободить системные ресурсы вызовом функции FreeMem можно только в том же самом модуле. Если попытаться вызвать функцию FreeMem в приложении (в частности, для примера, обсуждавше- гося выше), то происходит исключение. Поскольку при создании экземпляров класса всегда происходит обращение к диспетчеру памяти, то их деструкторы
308 Глава 7. Создание внутрипроцессных серверов автоматизации нельзя вызвать за пределами модуля. В частности, если в DLL происходит ис- ключение, то соответствующий объект создается в диспетчере памяти DLL. Если не перехватывать исключение, то этот объект попадает в приложение и после вы- вода диагностического сообщения приложение попытается его разрушить. При этом вновь произойдет исключение. Поэтому все экспонируемые в DLL функ- ции, в которых могут произойти исключения, должны иметь блоки перехвата ис- ключений: try {Основной код} {Возвращение ресурсов операционной системе} except On Е: Exception do begin ShowMessage(E.Message): { Возвращение ресурсов операционной системе. Здесь не следует использовать директиву RAISE} end: end; Ни в коем случае нельзя использовать директиву raise в секции except...end. В этой секции следует просто показать пользователю сообщение об исключении (или не показывать его, если условия работы приложения это позволяют). По этой же причине — из-за наличия разных диспетчеров памяти — нельзя использовать строки Delphi для передачи данных между модулями. Строки Delphi — это объекты и при изменении их содержимого происходит перераспре- деление памяти. Это вызовет исключение, если перераспределение памяти про- исходит в другом модуле. Поэтому для обмена текстовой информацией с DLL следует использовать переменные типа PChar. Типичный обмен текстовой информацией с DLL выглядит следующим обра- зом. Если необходимо передать какую-либо строку в функцию, содержащуюся в DLL, то можно просто использовать в функцию указатель на строку: procedure SendString(P: PChar); stdcall: external 'FirstLib.dll' name 'SendString'; procedure TForml.Button3C1ick(Sender: TObject); var S: String; begin S := 'This test string will be sended to DLL'; SendSt ring(PChar(S)); end; Функция SendString в DLL реализована следующим образом: procedure SendStringCP: PChar): stdcall; export; var S: String:
Создание и использование динамически загружаемых библиотек 309 begin S := StrPas(P): ShowMessage(S): end; При запуске данного примера появится сообщение с содержимым строки, созданной в исполняемом файле. Для того чтобы получить текстовую информа- цию из DLL, обычно в приложении создается буфер, который заполняется в DLL. Ясно, что размер буфера должен быть достаточным для хранения всей текстовой информации. Чтобы обезопасить себя от переполнения буфера, обычно вместе с буфером в качестве параметра посылается его размер. Типичный пример полу- чения текстовой информации из DLL выглядит следующим образом: procedure ReceiveString(Р: PChar; Size: Integer); stdcall; external 'FirstLib.dll' name 'ReceiveString': procedure TForml.Button4Click(Sender: TObject): var C: array[O..1000] of char; S: String; begi n ReceiveString(C, SizeOf(O); S := StrPas(C): Caption := S: end: Функция ReceiveString реализована в DLL: procedure ReceiveString(P: PChar; Size: Integer); stdcall; var S: String; N: Integer; begin FillMemory(P, Size, 0); if InputQuery('The string will be transferred '+ ' to application'. 'Type text', S) then begin N := Length(S); if N > Size then N := Size; if N > 0 then System.Move(S[l]. P[0], N); end: end: Именно таким образом работает большая часть функций Windows API, пре- доставляющих в приложения текстовые данные. Можно использовать и переменную типа PChar для получения указателя из DLL, но в этом случае в DLL должна быть объявлена глобальная переменная, предназначенная для хранения текстовой информации: procedure ReceiveBuffer(var Р: PChar); stdcall; external 'FirstLib.dll' name 'ReceiveBuffer';
310 Глава 7. Создание внутрипроцессных серверов автоматизации procedure TForml.Button5Click(Sender: TObject); var P: PChar; S; String; begin ReceiveBuffer(P): S := StrPas(P); Caption := S; end; В DLL данная функция реализована следующим образом: var {переменная не должна быть локальной!} Buffer:array[0..1000] of Char; procedure ReceiveBuffer(var P: PChar); stdcall: var S: String: N: Integer; begin FillMemory(@Buffer[0], SizeOf(Buffer). 0); if InputQueryCThe string will be transferred ' + ' to application',’Type text'.S) then begin N := Length(S); if N > Size0f(Buffer) - 1 then N := SizeOf(Buffer) - 1; if N > 0 then System.Move(S[l]. BufferfO], N); end; P ;= @Buffer[0]: end; Буфер нельзя определять как локальную переменную — после отработки функ- ции Recei veBuffer в DLL стек, куда помещаются локальные переменные, будет разрушен, и возвращаемый указатель будет ссылаться на недоступную область памяти. Аналогично, с помощью буфера можно передавать любые двоичные данные между приложением и DLL. Однако если размер двоичных данных варьируется в широких пределах, то могут возникнуть проблемы, связанные с размером буфера. Например, размер OLE-документов может варьироваться от нескольких десят- ков байтов до нескольких десятков мегабайтов. При использовании описанной выше технологии размер буфера должен быть равным максимально возможному размеру данных. Но если объявить буфер размером, скажем, 50 Мбайт, то большин- ство современных компьютеров будут создавать временное хранилище на диске. При этом передача даже небольших документов потребует заметного времени. Выход из данной ситуации заключается в резервировании памяти для хране- ния объекта в DLL и освобождении системных ресурсов в приложении, но без использования диспетчеров памяти приложения и DLL. Пример реализации этой идеи приведен ниже: procedure ReceiveWinAPKvar HMem:Integer); stdcall; external 'FirstLib.dll' name ’ReceiveWiпАРГ ;
Создание и использование динамически загружаемых библиотек 311 procedure TForml.Button6Cl1ck(Sender: TObject): var H: Integer: P: PChar: S: String: begin S := ReceiveWinAPI(H); if H > 0 then begin P := GlobalLock(H): S := StrPas(P): GlobalUnlock(H); GlobalFree(H): end; Caption := S; end; Реализация в DLL функции ReceiveWInAPI: procedure ReceiveWinAPKvar HMem: Integer); stdcall: export; var S: String; N: Integer; P: PChar; begin HMem := 0; if InputQuery(’The string will be transferred ’+ ’ to application’.'Type text'.S) then begin N := Length(S); if N > 0 then begin HMem := GlobalAlloc(GMEM_DDESHARE or GMEM_MOVEABLE or GMEM_ZEROINIT. N + 1): if HMem > 0 then begin P ;= Global Lock(HMem); if Assigned(P) then System.Move(S[l]. P[0], N); end; end; end; end; Здесь память резервируется в DLL, а освобождается в приложении. Для ре- зервирования и освобождения памяти используются соответственно функции GlobalAlloc и Global Free Windows API. Памяти резервируется ровно столько, сколько необходимо для хранения объекта. Для приведенного выше примера с OLE-документами это означает, что в большинстве случаев обращения к вирту- альной памяти на диске не потребуется, и, следовательно, обмен данными будет происходить быстро.
312 Глава 7. Создание внутрипроцессных серверов автоматизации Аналогично функциям Windows API можно использовать функции COM API для обмена данными между приложением и DLL. В COM API имеется пара функций CoTaskMemAlloc и CoTaskMemFree, которые определены в модуле ActiveX (мы уже обсуждали их в главе 1). Поскольку интерфейс COM API определен на уровне платформы, то вызовы этих функций можно успешно использовать для обмена данными между DLL и приложениями. Типичный пример кода для ре- зервирования памяти выглядит следующим образом: procedure ReceiveCOMAPKvar P: Pointer): stdcall; var S: String; N: Integer; begin P := nil; if InputQuery!'The string will be transferred ' + ' to application'. 'Type text', S) then begin N := Length(S); if N > 0 then begin P := CoTaskMemAlloc(Length(S) + 1); if Assigned!P) then begin FillMemory(P. N + 1. 0): System.Move(S[l], P\ N); end; end; end; end: Для резервирования памяти здесь используется вызов функции CoTaskMemAlloc. При этом выделяется не менее чем length(S)+l байтов памяти. Для проверки факта выделения полученный указатель сравнивается со значением nil. Выде- ленная память не инициализируется, поэтому обязательно следует использовать функцию Fill Memory. В основном приложении напишем следующий код обработчика событий, свя- занного со щелчком на кнопке: procedure ReceiveCOMAPKvar P:PChar); stdcall: external 'FirstLib.dll' name 'ReceiveCOMAPI’; procedure TForml.Buttonl7C1ick(Sender: TObject): var S: String: P: PChar: begin S := "; ReceiveCOMAPI(P): if Assigned(P) then begin S := StrPas(P):
Создание и использование динамически загружаемых библиотек 313 CoTaskMemFree(P); end; Caption := S; end; В этом фрагменте кода для освобождения ресурсов, зарезервированных в дру- гом модуле, используется процедура CoTaskMemFree. Из других средств обмена данными с DLL следует упомянуть модуль ShareMem. Именно сообщение о необходимости его использования можно прочитать в ком- ментарии, который генерирует мастер Delphi при создании DLL. Применение этого модуля позволяет создать общий диспетчер памяти для приложения и DLL. Ссылка на модуль ShareMem должна быть объявлена первой в секции uses, так как он подменяет существующий диспетчер памяти. Если этого не сделать и часть памяти будет выделена существующим диспетчером памяти, то при попытке ос- вободить ее в новом диспетчере получим исключение. Однако использовать этот модуль по следующим соображениям не рекомендуется: К при создании дистрибутива в комплект поставки необходимо включать файл BORLNDMM.DLL, что «утяжеляет» продукт; Ж понятно, что этот диспетчер памяти окажется работоспособным только в тех приложениях и библиотеках, которые написаны на языках программирова- ния, созданных компанией Borland, то есть теряется гибкость — мы не можем, например, вызывать созданную библиотеку из приложения, написанного на Visual Basic. В СОМ передача строк осуществляется в переменных типа WideString — строка, где для хранения одного печатного символа используется два байта, а передача двоичных данных осуществляется в переменных типа OLEVariant, содержащих массив байтов. Помимо этих формальных признаков имеется и другое сущест- венное отличие: ресурсы, хранимые в этих переменных и зарезервированные в одном модуле, могут быть корректно освобождены в другом модуле. Иногда использование переменных типа WideString или OLEVariant может быть нежела- тельным, так как в эти переменные осуществляется копирование данных. Если данные имеют большие размеры, то может возникнуть проблема с ресурсами. В этом случае следует реализовать доступ к данным через указатели, как это было описано в данном разделе. Вызов в DLL функций приложения Ранее рассматривались только варианты, когда функции DLL вызываются из приложения. EIo часто требуется, чтобы библиотека сама вызывала функции при- ложения — например, для передачи нотификационных сообщений. Когда необходимо вызвать из приложения функцию, не являющуюся мето- дом какого-либо класса, достаточно передать указатель на функцию в DLL: var NSum: Integer = 0; function CalculateSum(ReturnCaHback:pointer): Integer; stdcall:
314 Глава 7. Создание внутрипроцессных серверов автоматизации external 'FirstLib.dll’ name 'Sum': function GetNextValue: Integer: stdcall: begin if NSum < 200 then begin Inc(NSum): Result := NSum: end else Result := -1: end: procedure TForml.Button7C11ck(Sender: TObject): begin Caption := IntToStr(CalculateSum(@GetNextValue)); end: В приложении создается функция, не являющаяся методом класса, адрес ко- торой передается в DLL. При реализации таких функций — это так называемые функции обратного вызова (callback functions) — желательно использовать согла- шение о вызовах stdcall, чтобы их можно было бы вызывать из DLL, созданных на других языках программирования. Вызов функции в DLL можно проиллюст- рировать на примере: type TReturnNextMethod = function: Integer: stdcall; function CalculateSum(ReturnCallback: Pointer):Integer: stdcall: var N: Integer: ReturnNext: TReturnNextMethod: begin Result := 0: if ReturnCallback = nil then Exit; N := 0: ReturnNext := TReturnNextMethod(ReturnCallback): while N >= 0 do begin N := ReturnNext: if N >= 0 then Result := Result + N; end; end: При вызове метода объекта следует учитывать тот факт, что метод объекта характеризуется двумя адресами — адресом метода и адресом данных. Соответ- ственно, необходимо передавать два указателя. Вместо передачи двух указателей можно воспользоваться структурой TMethod, определенной в модуле SysUtils.pas: type TMethod = record Code. Data: Pointer; end:
Создание и использование динамически загружаемых библиотек 315 Код в приложении для приведенного выше примера выглядит следующим об- разом: type TGetNextValueObject = function: Integer of object; stdcall; function CalculateSumObject(Method: TMethod): Integer: stdcall: external 'FirstLib.dll' name 'SumObject'; function TForml.GetNextValueObject; Integer; stdcall; begi n if NSum < 10 then begin Inc(NSum): Result := NSum; end else Result := -1; end; procedure TForml.Button8Click(Sender: TObject); var FGetNext: TGetNextVa 1ueObject; begi n NSum ;= 0; FGetNext := GetNextValueObject: Caption := IntToStr(CalculateSumObject(TMethod(FGetNext))): end: Код в DLL, осуществляющий вызов метода объекта, выглядит следующим образом: type TReturnNextMethodObject=function:Integer of object; stdcal1: function CalculateSumObject(Method: TMethod): Integer; stdcall; var N: Integer; ReturnNext: TReturnNextMethodObject: begin Result ;= 0; N ;= 0; ReturnNext : = TReturnNextMethodObject(Method); while N >= 0 do begin N := ReturnNext: if N >= 0 then Result := Result + N; end; end:
316 Глава 7. Создание внутрипроцессных серверов автоматизации Следует учитывать, что методы объекта являются языково-зависимыми, то есть в разных языках программирования генерируются разные варианты кода для передачи данных в метод объекта. Поэтому представленный выше пример можно использовать, только если и приложение, и DLL созданы в Delphi (или написаны на одном и том же языке программирования). И, наконец, само приложение может экспонировать функции таким же спосо- бом, каким это делает DLL. В приложении можно создать секцию exports и объ- явить имена (и/или индексы) функций. После этого в DLL можно воспользовать- ся функцией GetProcAddress для получения указателя на функцию и вызвать ее. Для описанного выше примера код приложения выглядит следующим образом: function GetNextValueExport: Integer; stdcall: begin if NSum < 10 then begin Inc(NSum): Result := NSum: end else Result := -1; end: function CalculateSumExport(MethodName:PChar):Integer; stdcall; external 'FirstLib.dl1' name ’SumExport'; procedure TForml.Button9Click(Sender: TObject): var N: Integer; begin NSum := 0: N := CalculateSumExport('GetNextValueExport'); Caption := IntToStr(N): end; exports {Эта секция объявлена в исполняемом файле} GetNextValueExport index 1 name 'GetNextValueExport'; Обратите внимание — теперь в приложении (в проекте ЕХЕ-файла) опреде- лена секция exports! Соответствующий код в DLL для тестирования данной функции выглядит следующим образом: type TReturnNextMethodExport = function: Integer; stdcall: function Ca1culateSumExport(MethodName: PChar): Integer; stdcall: var ReturnNextExport: TReturnNextMethodExport: N: Integer: begin Result := 0: N := 0:
Создание и использование динамически загружаемых библиотек 317 // Если первый параметр нулевой. GetModuleHandle вернет // дескриптор файла, путем загрузки которого // создается вызывающий процесс. ReturnNextExport ; = GetProcAddress(GetNoduleHandle(nil), MethodName); while N >= 0 do begin N := ReturnNextExport; if N >= 0 then Result := Result + N; end: end; При запуске этого проекта приложение автоматически загружает DLL и на- ходит в DLL адрес функции CalculateNextExport (которая экспортируется по имени SumExport). При вызове этого проекта ему в качестве параметров передаются заголовок модуля приложения и имя функции, под которым она экспортируется из приложения. Далее, DLL использует функцию GetProcAddress для нахождения адреса функции, экспортируемой приложением. Для того чтобы был возвращен адрес функции, в приложении была объявлена секция exports, где описано внеш- нее имя функции. Этот пример иллюстрирует формально одинаковую структуру и способ формирования EXE- и DLL-файлов. В COM DLL вызовы функций клиента осуществляются путем создания но- тификационных интерфейсов (об этом рассказывалось в главе 3). Однако если COM DLL должна работать на том же компьютере, что и клиентское приложе- ние, то и сервер, и клиент имеют общее адресное пространство. В этом случае становятся значимыми указатели, в том числе указатели на функции. Поэтому серверу, реализованному в виде DLL, можно передавать указатели на функции клиента, и эти функции будут успешно вызываться. Работа с объектами в DLL Если в DLL необходимо передать указатель на объект, созданный в основном приложении, или наоборот, использовать в вызывающем приложении созданный в DLL объект, возникает проблема несовместимости объектов. Типичный пример кода, иллюстрирующий эту проблему, приведен ниже: procedure WhatObject(OB: Integer); var 0: TObject; begin 0 .- TObject(OB): if 0 is TButton then ShowMessage('Кнопка') el se ShowMessageC'Непонятно. что это такое'); end; procedure TForml.ButtonlClick(Sender: TObject);
318 Глава 7. Создание внутрипроцессных серверов автоматизации begin WhatObject(Integer(Sender)); end; Если оба этих метода реализованы в вызывающем приложении, то при щелчке на кнопке в форме появится сообщение «Кнопка». Если же метод WhatObject реализовать в DLL, то какой бы объект ни был указан в качестве параметра, все- гда будет появляться сообщение «Непонятно, что это такое». Таким образом, можно сделать абсолютно корректный вывод о том, что нельзя создать объект в одном модуле, а использовать его в другом. Почему работа с объектами в DLL реализована именно таким образом? Дело в том, что приложение и DLL могут быть реализованы на разных языках про- граммирования. Соответственно, даже при одинаковых названиях классов число переменных в классе и/или их размер и/или порядок их следования могут раз- личаться. То же самое относится и к методам класса. Поэтому в этом случае опе- ратор is всегда возвращает False, а оператор as генерирует исключение. И тем не менее в DLL можно использовать объекты, созданные в вызываю- щем приложении, и наоборот. Для этого необходимо, чтобы и DLL, и главное приложение были реализованы на одном и том же языке программирования. Ниже приведен пример подобного кода: procedure ChangeColor(FM: Integer): begin TForm(FM).Color := clGreen: end: procedure TForml.ButtonlClick(Sender: TObject); begin ChangeColorUnteger(Self)); end: Метод ChangeColor реализован в DLL, а метод ButtonlClick — в приложении. И DLL, и приложение созданы в Delphi. Вызов метода ChangeColor будет вполне корректно работать, если в качестве параметра указывать на объект типа TForm (или потомка TForm). Однако в DLL нельзя проверить тип объекта перед его при- ведением к типу TForm, поэтому, если использовать в качестве параметра другой объект, произойдет запись в непредсказуемое место в памяти, и хорошо, если сразу же будет сгенерировано исключение. Технология СОМ предоставляет альтернативный способ решения данной проблемы, позволяя создавать объекты в одном модуле, а применять в другом. Для этой цели используются интерфейсы. Примеры реализации интерфейсов приведены в главе 1. Модальные формы в DLL В DLL можно не только выполнять вычисления, но и показывать формы, напри- мер диалоговые окна. Для этого следует открыть проект реализации DLL, соз- дать модуль с формой и поместить на нее необходимые элементы управления
Создание и использование динамически загружаемых библиотек 319 вместе с обработчиками событий. Далее следует создать экспортируемую функ- цию, которая выведет диалоговое окно: var С: аггау[0..1000] of Char; function ExecDi alog(AppHandle:THandle; var PictName: PChar): Boolean: stdcall: var FDIalog: TForml: begi n FDIalog := nil: PictName := nil: Result := False: Application.Handle := AppHandle: {В этом случае на панели задач появятся две кнопки} try FDi alog:=TForml.Create(Appl1cati on); if FDialog.ShowModal = mrOK then begin FillMemory(@C[0], SizeOf(C). 0); if Length(FDialog.Editl.Text) > 0 then StrPCopyCC, FDialog.Editl.Text): PictName : = @C[0]: Result := True: end: FDialog.Release: FDialog := nil: (При динамической загрузке следует использовать метод Free вместо Release!} except On Е: Exception do begin ShowMessage(E.Message): if Assigned(FDialog) then FDialog.Release: end: end; end; Данный код вызова диалогового окна следует применять только при статиче- ской загрузке DLL. При реализации диалогового окна в DLL следует учитывать, что в отличие от приложений формы в DLL не могут создаваться одновременно с запуском DLL (в случае приложений для этого достаточно установки флажка Auto-Create Form на вкладке Forms диалогового окна, открываемого командой Project ► Options). Поэтому форму необходимо создавать динамически, вызывая ее конструктор Create из кода. Соответственно, перед выходом из процедуры, вызывающей форму, необходимо вызвать ее деструктор (в данном примере — FDialog.Release). Далее, следует учитывать, что в DLL создается объект типа TApplication. Поскольку само приложение тоже имеет данный объект, то, если не принимать никаких мер, на экране в панели задач появляются две кнопки —
320 Глава 7. Создание внутрипроцессных серверов автоматизации одна для приложения и другая для DLL, создающей диалоговое окно. Это иллю- стрирует рис. 7.2. Рис. 7.2. Появление двух кнопок на панели задач при вызове диалогового окна из DLL При щелчке мышью на кнопке приложения оно активируется, главная фор- ма появляется на экране, но доступ к элементам управления главной формы получить нельзя. Очевидно, такое поведение является некорректным. Поэтому в качестве параметра функции в библиотеке, создающей диалоговое окно, необ- ходимо использовать ссылку на объект TAppl icati on приложения (точнее, на его свойство Handle). Посредством следующего присвоения в DLL уничтожается объект TAppl 1 cati on, и приложение начинает поддерживать рассылку сообщений операционной системы элементам управления, созданным в DLL: Application.Handle := AppHandle: При этом на панели задач остается одна кнопка приложения, что вполне кор- ректно. Типичный пример кода основного приложения, вызывающего диалого- вое окно из DLL, приведен ниже: function ExecDialog(AppHandle: THandle: var PictName: PChar): Boolean: stdcall: external 'FirstLib.dll' name 'ExecDialog': procedure TForml.ButtonlOC11ck(Sender: TObject): var P: PChar; S: String: begin if ExecDialog(Application.Handle, P) then begin S := P: Caption := S: end; end;
Создание и использование динамически загружаемых библиотек 321 Если загружать DLL динамически, возникает ряд проблем. Первая из них за- ключается в том, что, как правило, загрузка и выгрузка библиотеки, из которой вызывается диалоговое окно, осуществляется в пределах одной функции: HLib := LoadLibraryC'FirstLib.dll'): if HLib <> 0 then begin ExecDialog:=GetProcAddress(HLib. 'ExecDialog'): if Assigned(ExecDialog) then begin if ExecDialog(Application.Handle. P) then {...}; end else ShowMessagei'Method with name ExecuteDialog'+ ' was not found'); FreeLibrary(HLib); end else ShowMessage('Can not load library FirstLib.dll'): При этом библиотека выгружается немедленно после закрытия диалогового окна, а метод Release, который рекомендуется использовать для вызова деструктора формы, посылает сообщения CM_RELEASE форме вызовом функции PostMessage. Эта функция ставит сообщение в конец очереди, приложение продолжает выполнять код — и выгружает DLL! Только после выполнения кода начинает обрабаты- ваться очередь сообщений, в конце концов из очереди извлекается сообщение CM_RELEASE и делается попытка выполнить деструктор формы — а функции-то уже выгружены! Если система имеет значительный резерв ресурсов, то велика вероят- ность, что место в памяти, где хранился код, сохранит свое содержимое и форма будет разрушена успешно. Но при малых ресурсах в освободившемся месте в опе- ративной памяти немедленно окажутся какие-либо данные из виртуальной дис- ковой памяти, и попытка выполнить деструктор закончится исключением. По- этому при динамической загрузке обязательно следует использовать метод Free вместо Release. Кроме того, перед выгрузкой DLL рекомендуется вызвать метод Appl1cati on.ProcessMessages. Другая проблема заключается в использовании одного и того же объекта TApplication и в приложении, и в DLL при выполнении приведенного выше опе- ратора: Application.Handle := AppHandle: Если выполнить этот оператор перед вызовом метода ShowModal в DLL, то по- сле выгрузки DLL главная форма приложений сворачивается и ее необходимо восстанавливать вновь. Один из способов решения этой проблемы — вызвать функцию ShowWindow(Handle, SW_RESTORE) сразу же после выполнения команды FreeLibrary в приложении. Однако при этом главная форма приложения будет мерцать. Другой способ — оставить разные объекты TApplication в приложении и DLL — для этого указанный выше оператор в DLL выполнять не надо. Однако при этом на панели задач появляются две кнопки. Корректное решение про- блемы заключается в присвоении нулевого значения свойству Application.Handle в DLL перед выходом из функции. Ниже приведен рекомендуемый код для библиотеки, выводящей диалоговое окно при динамической загрузке:
322 Глава 7. Создание внутрипроцессных серверов автоматизации function ExecDialog(AppHandle: THandle; var PictName: PChar):boolean: stdcall: var FDialog: TForml; begin FDialog := nil: PictName := nil: Result := False: Application.Handle := AppHandle: try FDialog := TForml.Create(Application): if FDialog.ShowModal = mrOK then begin FillMemory(@C[0]. SizeOf(C), 0); if Length(FDialog.Editl.Text) > 0 then StrPCopy(C. FDialog.Editl.Text): PictName := @C[0]: Result := True: end; FDialog.Free: FDialog := nil; except On E: Exception do begin ShowMessage(E.Message); if Assigned(FDialog) then FDialog.Free: end; end; Appl1cati on.ProcessMessages: Application.Handle := 0; end: Ниже приведен код приложения, динамически вызывающего данную библио- теку: type TExecDialog = function(AppHandle: THandle: var PictName: PChar): Boolean: stdcall: procedure TForml.ButtonllClick(Sender: TObject): var ExecDialog: TExecDialog: HLib: THandle: P: PChar: S: String: begin HLib := 0; try HLib := LoadLibrary('FirstLib.dll'): if HLib о 0 then begin ExecDialog := GetProcAddress(HLib. 'ExecDialog');
Создание и использование динамически загружаемых библиотек 323 if Assigned(ExecDialog) then begin if ExecDialogCApplication.Handle. P) then begin S := P: Caption := S; end: end else ShowMessage('Method with name '+ ' Executed al og was not found'): end else ShowMessageC'Can not load library ’+ ' FirstLib.dll'): finally Appli cati on.ProcessMessages: if HLib <> 0 then FreeLibrary(HLib): end; end; Отображение модальных форм редко осуществляется в COM DLL — обычно в COM DLL помещают расчетные методы. Но можно показывать и модальные формы — при этом не возникают проблемы со вторым экземпляром объекта TApplication. В COM DLL также следует динамически вызывать конструктор модальной формы. Немодальные формы в DLL Отображение немодальных форм традиционно осуществляется со статической загрузкой DLL. Типичный код для отображения немодальной формы в DLL вы- глядит следующим образом: procedure ShowNonModalFornKAppHandle: THandle); stdcall: begin Appli cati on.Handle:=AppHandl e: with TForm2.Create(Application) do Show; end; Так же как и при отображении модальных форм, необходимо присвоить деск- риптор (handle) окна главного приложения тому приложению, которое создается в DLL, — иначе на панели задач будут показаны две кнопки. Далее просто созда- ется форма и вызывается ее метод Show. Данный способ отображения немодаль- ных форм приводит к тому, что из главного приложения указанную функцию можно вызывать неоднократно, создавая тем самым несколько экземпляров формы. Зачастую это оправдано — формы одинакового типа могут содержать, например, разные документы. Но при таком способе отображения рекомендуется сделать обработчик события OnClose для TForml и параметру CloseAction присвоить значе- ние caFree — иначе при закрытии формы она будет исчезать с экрана без освобо- ждения системных ресурсов. Для отображения единственного экземпляра немодальной формы следует не- много изменить код: procedure ShowSingleNonModalForm(AppHandle:THandle): stdcall: begin Appli cati on.Handle:=AppHandle:
324 Глава 7. Создание внутрипроцессных серверов автоматизации if AssignedCForml) then Forml.Show else begin Forml:=TForml.CreateCAppl1cati on): Forml.Show; end; end; В приведенном выше фрагменте кода первоначально проверяется, создана ли уже эта форма, и если создана — то просто вызывается ее метод Show. В противном случае тот же метод вызывается после отработки конструктора. Вызов метода Show для уже созданного экземпляра формы имеет смысл потому, что пользова- тель может обратиться к команде отображения формы в тех случаях, когда уже имеющийся экземпляр перекрыт другими окнами и не заметен на экране — вызов метода Show ведет к «всплытию» формы. Переменная Form2 является глобальной. Оба описанных выше способа вызова немодальных форм не требуют созда- ния специальной процедуры для их разрушения — ресурсы будут корректно ос- вобождены при закрытии приложения, так как приложение является владельцем форм. Код приложения для тестирования этих способов вызова выглядит сле- дующим образом: procedure ShowNonModalFormCAppHandle: THandle): stdcall; external 'FirstLib.dll' name 'ShowNonModalForm'; procedure ShowSingleNonModalForm(AppHandle: THandle): stdcall; external 'FirstLib.dll' name 'ShowSingleNonModalForm’: procedure TForml.Buttonl2Click(Sender: TObject): begin ShowNonModalForm(Appli cati on.Handle); end; procedure TForml.Buttonl3Click(Sender: TObject): begin ShowSi ngleNonModalForm(Appli cati on.Handle); end: Однако иногда возникает необходимость отображения немодальных форм из динамически загружаемых библиотек — например, при редком использовании в приложении немодальных форм для экономии ресурсов. Если реализовать код таким же образом, как и при показе модальных диалоговых окон, то форма будет создана и, может быть, даже показана на экране. Но после этого произой- дет выгрузка DLL, и вслед за этим немедленно последуют исключения, так как в памяти компьютера будет отсутствовать код для работы с элементами управле- ния формы. Традиционное решение этой проблемы выглядит следующим образом: библиотека загружается динамически, и в качестве одного из параметров ей переда- ется адрес функции главного приложения, которая будет вызвана при закрытии немодальной формы — обычно в обработчике события OnDestroy. Эта функция должна информировать главное приложение о необходимости выгрузки DLL из
Создание и использование динамически загружаемых библиотек 325 памяти компьютера, но библиотека должна выгружаться после завершения ее работы (и, следовательно, после завершения работы деструктора формы) — иначе возможно возникновение исключения из-за отсутствия кода в памяти компьютера. Это достигается путем асинхронной развязки — посылки сообщения с помощью функции PostMessage какому-либо окну приложения, обычно главной форме. Код реализации данного способа отображения немодальных форм в DLL выглядит следующим образом: type TNotifyClose = procedure: stdcall; TForml = class(TForm) Memol: TMemo; procedure FormDestroy(Sender: TObject): procedure FormClose(Sender: TObject: var Action: TCloseAction); public FNC: TNotifyClose; end; var Forml: TForml; procedure DynNonmodal(AppHandle: THandle; NC; Pointer); stdcal1: implementation procedure TForml.FormDestroy(Sender: TObject): begin . if Assigned(FNC) then FNC; end; procedure TForml.FormCloseCSender: TObject; var Action: TCloseAction): begin Action := caFree: end; procedure DynNonmodal(AppHandle: THandle; NC; Pointer): stdcal1; begin Application.Handle := AppHandle; if Assigned(Forml) then Forml.Show else begin Forml := TForml.Create(Application);
326 Глава 7. Создание внутрипроцессных серверов автоматизации Forml.FNC := TNotifyClose(NC); Forml.Show; end: end; Приложение, использующее эту библиотеку, имеет следующий код (константа WM_DLLUNLOAD определена в секции interface модуля): type TDynNonmoda1“procedure(AppHand 1 e: THandle: NC: Pointer); stdcall: procedure ReceiveCloseNotify: stdcall; begi n Appli cati on.ProcessMessages; PostMessage(Forml.Handle. WM_DLLUNLOAD. 0, 0); end; procedure TForml.WMDLLUnload(var Message:TMessage); begin Application.ProcessMessages; ’ if FHLib <> 0 then FreeLibrary(FHLIB); FHLib ;= 0: ShowMessage('Library unloaded'); end; procedure TForml.Buttonl4Click(Sender: TObject); var DM: TDynNonmodal; begi n if FHLIB = 0 then FHLib ;= LoadLibraryCNMDynl.dll ’); if FHLib > 0 then begin DM := GetProcAddress(FHLib. 'DynNonmodal'); if Assigned(DM) then DM(Application.Handle. @ReceiveCloseNotify); end: end; Данный код получается довольно громоздким: в главном приложении не- обходимо реализовывать три функции вместо одной. Альтернативный вариант можно предложить, исходя из того, что в DLL имеется объект TAppl i cati on, кото- рый может поддерживать цикл выборки сообщений. Но в DLL нельзя создать форму, используя метод TApplication.CreateForm, так как вкладка Forms диалого- вого окна Options (вызывается командой Project ► Options) отсутствует в проектах Delphi 4, 5, 6, 7 и недоступна в Delphi 3. Однако все методы объекта TApplication можно вызвать вручную, дописав соответствующий код в DLL:
Создание и использование динамически загружаемых библиотек 327 procedure ShowNMApplication; stdcall; begin if Assigned(Forml) then begin Forml.Show; Exit; end else begin Application.Initialize: Application.CreateForm(TForml. Forml): Application.Run; Forml.Free; Forml := nil: end: end; Следует обратить внимание на то, что в данном проекте дескриптор главного приложения не присваивается дескриптору TAppl ication в DLL. Это реально при- водит к появлению двух кнопок на панели задач. Но в некоторых случаях это по- лезно — так легче добраться до окон, перекрытых другими окнами. Интересно, что в Delphi 3 после написания данного кода становятся доступными элементы управления вкладки Forms диалогового окна Options, где можно определить авто- матически создаваемые формы и главную форму приложения. В более поздних версиях Delphi такая возможность отсутствует. Код главного приложения, использующий данную библиотеку, выглядит сле- дующим образом: type TShowApp = procedure; stdcall; procedure TForml.Buttonl5Click(Sender: TObject); var HLib: THandle: ShowApp: TShowApp: begi n HLib:=LoadLibrary('NMDyn2.dll’): if HLib <> 0 then begin ShowApp := GetProcAddress(HLib. 'ShowNMApplication'): if Assigned(ShowApp) then ShowApp; Appl i cati on.ProcessMessages; FreeLibrary(HLib); end: end; В отличие от предыдущего примера, динамическая загрузка библиотеки и ее выгрузка осуществляются в одной функции, да и объем написанного кода суще- ственно меньше. В COM DLL немодальные формы не отображаются, хотя, скорее всего, это можно сделать.
328 Глава 7. Создание внутрипроцессных серверов автоматизации Экспорт дочерних форм из DLL Дочерние формы (формы, которые а качестве родителей имеют другие формы, а не экран компьютера) достаточно распространены в приложениях. В принципе, такие формы могут поставляться и в DLL. При этом используется статическая загрузка DLL. Динамическая загрузка DLL возможна только в том случае, если библиотека загружается сразу же вместе с созданием формы, на которой разме- щается дочерняя форма, и выгружается при закрытии этой формы. Следует принять во внимание тот факт, что дочерняя форма может быть за- действована в нескольких формах главного приложения с различающимся жиз- ненным циклом. Поэтому помимо функции, которая будет создавать дочернюю форму, необходимо иметь функцию для ее разрушения и возврата занятых ресур- сов операционной системе. При этом опять же, поскольку возможно существова- ние нескольких дочерних форм, при создании каждой формы главному прило- жению должен быть сообщен ее идентификатор, который необходимо сохранить и использовать при вызове деструктора данной дочерней формы. Исходя из этих теоретических предпосылок, можно приступить к созданию библиотеки, способ- ной экспортировать дочерние формы: function CreateCustomWindowCParentHandle: Integer; DataRect: TRect; var WinHandle: THandle): Integer: stdcal1; {Функция возвращает дескриптор формы, который должен быть передан деструктору} var FD:TForml; begin Result := 0: WinHandle := 0: try FD := TForml.Create(nil); Result := Integer(FD); WinHandle := FD.Handle; if ParentHandle <> 0 then begin SetParentIWinHandle, ParentHandle); with FD do begin SetWindowPosIHandle. HWND_TOP, DataRect.Left. DataRect.Top, DataRect.Right - DataRect.Left. DataRect.Bottom - DataRect.Top. SWP_SHOWWINDOW); Show; end: end; except on E: Exception do MessageDlg(E.Message. mtError. [mbOK], 0); end; end:
Создание и использование динамически загружаемых библиотек 329 procedure DeleteCustomWindow(Win ID: Integer); stdcall; begi n try if WinlD <> 0 then TForml(WinlD).Free; except on E: Exception do MessageDlglE.Message. mtError. [mbOK], 0): end; end; На форму TForml помещают элементы управления, которые необходимо пока- зать в главном приложении (рис. 7.3). Обычно свойство BorderStyle дочерних форм устанавливают равным bsNone. Рис. 7.3. Форма в виде DLL, экспортируемая как дочерняя Далее, дочерним формам в качестве параметров функции следует передавать прямоугольную область родительского окна, в которой они размещаются. Это озна- чает, что дочерняя форма должна иметь хорошо отлаженные обработчики событий, которые связаны с изменением ее размеров. Как и все предыдущие примеры, код показа дочерних форм следует сделать языково-независимым — а это значит, что необходимо использовать функции Windows API для изменения свойств их роди- телей. Такая функция определена в модуле user32.dll — SetParent. Другая функция Windows API — SetWindowPos — требуется для изменения границ формы. Воз- вращаемым параметром является указатель на объект. Однако задействовать его сразу приложение не может — оно должно запомнить этот указатель и использо- вать при вызове функции Del eteCustomWi ndow, — поэтому сохраняется возможность применения данного проекта в приложениях, написанных не на Delphi. Еще один полезный параметр — дескриптор окна формы — передается как вариантный параметр функции CreateCustomWi ndow. Он может быть использован главным приложением для посылки сообщений форме, динамического измене- ния размеров, атрибутов видимости и т.’д. посредством вызова соответствующих функций Windows API. Код основного приложения для тестирования данной DLL выглядит следую- щим образом: TForm2 = class(TForm) procedure FormCloselSender: TObject: var Action: TCloseAction); procedure FormCreatelSender: TObject): procedure FormDestroy(Sender: TObject):
330 Глава 7. Создание внутрипроцессных серверов автоматизации private FHLib: THandle; FChildID: Integer; FChildHandle: THandle; end: var Form2: TForm2; implementation type TCreateCustomWindow = function!ParentHandle: Integer: DataRect: TRect; var WinHandle: THandle): Integer; stdcall; TDeleteCustomWindow = procedure(WinID: Integer): stdcall; procedure TForm2.FormClose(Sender: TObject: var Action: TCIoseAction); begin Action := caFree; end: procedure TForm2.FormCreate(Sender: TObject); var CreateW:TCreateCustomWindow; begin FHLib := LoadLibrary!'ChildDLL.dll’); if FHLib <> 0 then begin CreateW := GetProcAddress(FHLib. 'CreateCustomWindow'); if Assigned(CreateW) then FChildID := CreateW(Handle. ClientRect. FChildHandle); end: end: procedure TForm2.FormDestroy(Sender: TObject); var DeleteW: TDeleteCustomWindow; begi n if (FChildID > 0) and (FHLib <> 0) then begin DeleteW := GetProcAddress(FHLib, 'DeleteCustomWindow'); if Assigned(DeleteW) then DeleteW(FChildlD); end; if FHLib о 0 then FreeLibrary(FHLib); end; На форму TForm2 в этом проекте никаких элементов управления не помещается. Главная форма приложения содержит одну кнопку, при щелчке на которой про- исходит немодальное отображение формы TForm2: with TForm2.Create(Self) do Show;
Внутрипроцессный сервер автоматизации 331 При создании второй формы выполняется загрузка DLL, и форма, созданная в DLL, становится дочерней для TForm2. Можно создать несколько экземпляров TForm2. При разрушении конкретного экземпляра разрушается и дочернее окно на нем — для этого используется сохраненный ранее параметр FChildID (рис. 7.4). [tditl Button! Edit! Bidtonl BUtonl Рис. 7.4. Отображение в главном приложении дочерней формы, экспортируемой из DLL Казалось бы, аналогичную методику можно использовать для экспорта из DLL других элементов управления, среди предков которых нет класса TForm. Однако при попытке вызвать функцию SetParent непосредственно для элемента управления происходит генерация исключения, связанного с отсутствием роди- тельского окна у элемента управления, и в результате он не отображается на форме. В COM DLL часто используют дочерние формы. Однако код их создания аб- солютно не похож на представленный здесь. Дочерние формы COM DLL — это элементы управления ActiveX, работа с которыми была описана в главе 2. Внутрипроцессный сервер автоматизации Как мы уже говорили выше, нередко сервер автоматизации создается в виде DLL. Главное отличие такого сервера автоматизации заключается в том, что все объекты, содержащиеся в DLL, находятся в адресном пространстве приложения, которое к ним обращается. Поэтому сервер автоматизации, расположенный в DLL, на- зывают внутрипроцессным сервером, в то время как сервер, расположенный в исполняемом файле, называют внепроцессным сервером.
332 Глава 7. Создание внутрипроцессных серверов автоматизации Внутрипроцессный сервер может манипулировать объектом, который был создан процессом в исполняемом файле, если на него передан указатель. Вне- процессный сервер при попытке использовать указатель на объект, созданный другим процессом, в лучшем случае генерирует сообщение об ошибке доступа к памяти (Access Violation). Для передачи такого указателя необходимо ис- пользовать интерфейс IMarshall, который находит указатель на объект, создан- ный одним процессом в адресном пространстве другого процесса. Об интерфейсе IMarshall рассказывалось в главе 1. Указатель невозможно передать через стандартные переменные: в языке IDL и в спецификации, описывающей правила создания серверов и контроллеров ав- томатизации, отсутствует переменная типа pointer. Однако можно передать пе- ременную типа Integer, затем преобразовать ее к типу pointer и манипулировать этим объектом. Пример передачи указателя через переменную типа Integer при- веден далее в этой главе. Создание внутрипроцессного сервера автоматизации всегда начинается с созда- ния библиотеки ActiveX. Для этого необходимо выбрать команду File ► New ► Other, перейти на страницу ActiveX репозитария объектов и выбрать значок ActiveX library (как это делалось при создании элементов управления ActiveX). Встроенный мастер создаст новый проект: library Projectl; uses ComServ; exports DllGetClassObject. DllCanUnIoadNow. Dll Registerserver. DI 1Unregi sterServer: {$R *.RES} begi n end. Этот проект будет генерировать код для создания DLL, причем внешним при- ложениям будут видны четыре функции. Реализация этих функций приведена в модуле ComServ. Следующая функция создает фабрику классов для данного идентификатора класса (CLSID) и возвращает уникальный идентификатор ин- терфейса (IID) в переменной Obj приложению, вызвавшему эту функцию: DllGetClassObject(const CLSID. IID: TGUID: var Obj): HRESULT Другая функция вызывается операционной системой для того, чтобы устано- вить, можно или нет выгрузить DLL из памяти и вернуть ресурсы операционной системе (если данную библиотеку используют какие-либо процессы): DllCanUnIoadNow: HRESULT
Внутрипроцессный сервер автоматизации 333 Наконец, две последние функции требуются для записи данных о сервере в сис- темном реестре и для удаления этих записей оттуда. Обратите внимание на то, что программисту не следует писать код реализа- ции этих функций, так как он уже содержится в модуле ComServ. Если же необхо- димо эти функции переопределить, то можно написать их реализацию, но она обязана включать все то, что имеется в реализации ComServ, и список формаль- ных параметров этих функций должен быть тем же самым, что и в ComServ. Затем следует вновь обратиться к команде File ► New ► Other и на странице ActiveX репозитария выбрать значок Automation Object. Так же как и в случае вне- процессного сервера автоматизации, появится диалоговое окно, в котором следует определить имя класса и способ создания фабрики классов при создании СОМ- объекта — будет ли создаваться новая копия (Single Instance) или будет возвра- щаться ссылка на имеющуюся копию (Multiple Instance). Поскольку одна библиотека может обслуживать несколько приложений, выберем значение Multiple Instance. В раскрывающемся списке Threading model выбирают модель потоков, то есть способ, которым будет осуществляться доступ к серверу автоматизации при работе процесса в многопоточном режиме. Подробно работа в многопоточном режиме обсуждается в главе 6. В отличие от внепроцессного сервера автоматизации, в дан- ном случае значение этого параметра никак не влияет на компилируемый код — изменяются только данные, заносимые в системный реестр. Флажок Generate Event support code позволяет обеспечить поддержку ноти- фикационных сообщений (уведомлений) от сервера автоматизации. При уста- новке этого флажка будет создан диспитерфейс, в котором можно определять нотификационные сообщения. Работа с нотификационными сообщениями была рассмотрена в главе 3. После заполнения диалогового окна будет создана библиотека типов и мо- дуль, где необходимо создать реализацию конкретных методов, так же как и для внепроцессного сервера автоматизации. В библиотеке типов создается интер- фейс, в котором следует определять свойства и методы, а также диспинтерфейс, если реализуется поддержка нотификационных сообщений. Далее можно опре- делить новые свойства и методы, так же как это было сделано при создании вне- процессных серверов автоматизации. Сервер автоматизации необходимо регистрировать в системном реестре. Для этого из какого-либо приложения вызывают определенную ранее функцию DllRegisterServer. Ниже приведен фрагмент кода: type TDLLReglsterServer = function: HRESULT; stdcall; procedure TForml.RegisterServerClick(Sender: TObject); var LibName: String; HLib; THandle: RServ; TDLLReglsterServer: begin LibName := Editl.Text;
334 Глава 7. Создание внутрипроцессных серверов автоматизации HLib := LoadLibrary(PChar(LibName)): if HLib <> 0 then begin RServ := GetProcAddressCHLib. 'DllRegisterServer'): if Assigned(RServ) then begin if RServ = S_0K then ShowMessage(Format('Server Xs was successfully '+ ' regi stered',[LibName])) else ShowMessage(Format('Can not register server Xs', [LibName])): end else ShowMessage('Method DllRegisterServer '+ ' does not supported in the selected library'); FreeLibrary(HLib): end else ShowMessage(Format('Library Xs was not found', [LibName])): end; В каталоге WINNT/System32 имеется приложение Regsvr32.exe. При помощи этого приложения можно регистрировать внутрипроцессные серверы автомати- зации в системном реестре и наоборот, удалять сведения о них из реестра. Ниже приведены примеры соответствующих команд: ж регистрация сервера: с:\winnt\system32\regsvr32 с:\symb\astruct\axstrcnt.dll Ж удаление записей для данного сервера из системного реестра: c:\winnt\system32\regsvr32 /и c:\symb\astruct\axstrcnt.dll При регистрации COM DLL в системном реестре в секции InProcServer32 описывается модель потоков, если в раскрывающемся списке Threading model не выбран пункт None. Обработка ошибок Соглашения о вызовах были рассмотрены выше в подразделе «Создание про- стейшей библиотеки» раздела «Создание и использование динамически загру- жаемых библиотек» и там же было сказано, что в COM-интерфейсах используют соглашения stdcall и safecall. Однако из приведенной там табл. 7.1 нельзя по- нять разницу между этими соглашениями — оба они помещают параметры в стек справа налево, и в обоих стек очищает вызываемый метод. Разница между этими соглашениями заключается в том, что директива safecall не только определяет соглашение о вызовах, но осуществляет обработку ошибок в СОМ-интерфейсах. Сразу же следует оговориться, что описанная ниже обработка ошибок верна как для внутрипроцессного сервера автоматизации (DLL), так и для внепроцессного сервера (EXE), но во внутрипроцессных серверах автоматизации директива safecal 1 несет еще и информацию о работе со стеком. Базовая модель обработки ошибок СОМ заключается в том, что все методы представляют собой функции, возвращающие значение типа HRESULT — целое
Обработка ошибок 335 число. При успешном завершении функции она должна вернуть значение S_OK (0), в случае ошибки — код ошибки. Кроме того, COM-сервер может предоставить по запросу клиента дополнительную информацию об ошибке при помощи мето- дов реализованных им интерфейсов ISupportErrorlnfo и lErrorlnfo. Такой стиль программирования привычен для разработчиков на языке C++, однако плохо согласуется с принятой в Delphi моделью обработки ошибок, связанной с ис- ключениями. Именно для устранения этого противоречия в Delphi введен дополнительный тип соглашения о вызовах, задаваемый директивой safecall. По умолчанию он используется для реализации функций объектов ActiveX и при импорте библио- тек типов. Впрочем, это поведение можно изменить при помощи диалогового окна Environment Options (рис. 7.5). biVironffient sptKrfii Presences Designet | Object Inspector Typelibrary | EnvisamerM Variables p- SafeCall function mapphg.... ........! J ». Ail v-tabte interfaces E 1 <*• Ony dual interfaces ! О Op not map Рис. 7.5. Настройка соглашения о вызовах safecall Целевые интерфейсы для соглашения о вызовах safecall выбираются с помо- щью группы переключателей SafeCall function mapping: Ж All-v-table interfaces — соглашение о вызовах safecall поддерживается для всех интерфейсов; в Only dual interfaces — соглашение о вызовах safecall поддерживается только для дуальных интерфейсов (реализующих интерфейс IDispatch); Ж Do not map — соглашение о вызовах safecall не поддерживается. У внимательного читателя уже, наверное, возник вопрос: «Как же тогда осу- ществляется взаимодействие с серверами и клиентами, созданными не в Delphi? Ведь они не поддерживают соглашения о вызовах safecall». Правильно. А ответ прост — компилятор Delphi тоже не компилирует такие методы во что-то уни- кальное. Они преобразуются в обычные функции, возвращающие значение типа HRESULT и поддерживающие соглашение о вызовах stdcall, но с некоторыми до- полнительными изменениями, которые мы сейчас рассмотрим. Соглашение о вызовах safecall на клиенте Если при импорте библиотеки типов сгенерированы методы, поддерживающие соглашение о вызовах safecall, то их вызов преобразуется в следующий псевдокод: var HR: HRESULT:
336 Глава 7. Создание внутрипроцессных серверов автоматизации begin HR := StdCalllmplementationOfFunctionCParaml. Param2 ...); if not Succeed(HR) then begin 11 Получение от сервере информации об ошибке raise Е01eError.Create(...); end; end; Вызов метода сервера осуществляется согласно стандартам СОМ как функ- ции, возвращающей значение типа HRESULT. Соглашение о вызовах safecall на сервере Если мы реализовали метод СОМ-сервера как процедуру, поддерживающую со- глашение о вызовах safecall, Delphi скомпилирует его в функцию, возвращающую значение типа HRESULT и поддерживающую соглашение о вызовах stdcall. Если ме- тод был функцией, то возвращаемое значение преобразуется в еще один параметр этого метода. Сама функция будет реализована в виде следующего псевдокода: try <тело функции» Result := S_OK except 11 Подготовка информации для lErrorlnfo Result := <код ошибки» end: Благодаря этому можно свободно генерировать исключения в коде функции, и они будут преобразованы в ошибки СОМ. Тестовая программа Для иллюстрации преобразований, необходимых для перехода от соглашения safecall к соглашению stdcall, на компакт-диске есть небольшой пример. В нем создано два идентичных по набору методов СОМ-сервера, причем в одном их них методы реализованы в соответствии с соглашением о вызовах safecall, а в дру- гом — с соглашением о вызовах stdcall: ISafeCal1Object = interface!IUnknown) ['{B17150B0-D067-11D5-BAFB-AD70D683031F}'] procedure Add(A: Integer; B: Integer; out C: Integer); safecal 1: procedure Call Error; safecall; procedure BadHRESULT; safecall; end; TSafeCallObject = class(TTypedComObject. ISafeCallObject) protected procedure Add(A. B: Integer; out C: Integer): safecall:
Обработка ошибок 337 procedure Call Error: safecall; procedure BadHRESULT: safecal1: end: Метод Add демонстрирует успешное завершение: procedure TSafeCallObject.Add(A, В: Integer: out C: Integer): begi n C := A + B: end: Метод Call Error генерирует исключение: procedure TSafeCal1Object.CallError: begin raise Exception.Create!'My exception raised'); end; Метод BadHRESULT возвращает значение типа HRESULT с кодом ошибки. Явно сде- лать это в методе, поддерживающем соглашение о вызовах safecall, невозможно: procedure TSafeCal1 Object.BadHRESULT; begin 01eError(E_FAIL); end: IStdCallObject = interface!IUnknown) ['{B17150B5-D067-11D5-BAFB-AD70D683031F}' ] function Add(A: Integer; B: Integer; out C: Integer): HRESULT: stdcall: function Call Error: HRESULT; stdcall; function BadHRESULT: HRESULT: stdcall: end: TStdCallObject = class(TTypedComObject, IStdCallObject) protected function Add(A, B: Integer: out C: Integer): HRESULT; stdcall; function BadHRESULT: HRESULT; stdcall; function Call Error: HRESULT: stdcall; end; Метод Add, как и в предыдущем случае, завершается успешно: function TStdCal1 Object.Add!A, B: Integer: out C: Integer): HRESULT; begin С := A + B: Result := S_OK: end;
338 Глава 7. Создание внутрипроцессных серверов автоматизации Метод BadResult возвращает значение типа HRESULT с кодом ошибки, на этот раз явно: function TStdCa11 Object.BadHRESULT: HRESULT: begin Result := E_FAIL: end: Метод Call Error по-прежнему генерирует исключение Delphi: function TStdCallObject.Call Error: HRESULT: begin raise Exception.Create('Exception raised'): end: Затем библиотеки типов каждого из серверов были импортированы при раз- личных параметрах импорта, чтобы получить библиотеки с поддержкой обоих соглашений о вызовах (safecall и stdcall). И, наконец, были реализованы два клиента, обращающиеся к серверам всеми возможными способами. Первый клиент создан с библиотеками типа, импортированными с поддерж- кой соглашения о вызовах stdcall. Он создает экземпляры обоих объектов и вы- зывает их методы: procedure TfStdCall.FormCreate(Sender: TObject): begin SafeCa11 Object := CoSafeCallObject.Create: StdCallObject := CoStdCallObject.Create: end: Результаты взаимодействия клиента, поддерживающего соглашение о вызо- вах stdcall, с сервером, поддерживающим соглашение о вызовах stdcall, представ- лены на рис. 7.6. Метод Add, как и ожидалось, возвращает значение типа HRESULT, равное 0: procedure TfStdCall.StdCal1AddClэck(Sender: TObject); var C: Integer: H: HRESULT; begin H := StdCallObject.Add(5. 8. C); Memol.Lines.Add(Format!'StdCall.Add С=И. HRESULT=£x'. [С. H])); end: Метод BadHRESULT возвращает ожидаемый код ошибки: procedure TfStdCall.StdCallBadHRESULTClick(Sender: TObject): var H: HRESULT:
Обработка ошибок 339 begin Н := StdCal10bject.BadHRESULT; Memol.Lines.Add(Format('StdCall.BadHRESULT HRESUEMx'. EH])); end; Рис. 7.6. Результат работы клиента и сервера, поддерживающих разные соглашения о вызовах Наиболее интересен результат вызова метода Call Error. Он генерирует ис- ключение Delphi, однако в клиентское приложение все равно приходит значение типа HRESULT. Это значение сгенерировано СОМ — именно СОМ обрабатывает исключение при вызове метода. Такой результат будет получен, только если клиент и сервер находятся в разных апартаментах. При нахождении их в одном апартаменте (внутрипроцессный сервер совместим с клиентом по моделям пото- ков) вызов идет напрямую по интерфейсной ссылке в обход цепочки объектов прокси-стаб, и ответственность за обработку исключения ложится на клиента. Если он не обрабатывает исключений, происходит аварийное завершение потока клиента. Поэтому при написании методов сервера, поддерживающих соглашение о вызовах stdcall, не стоит «выпускать» за его пределы функции необработанных исключений. procedure TfStdCall.StdCallCallErrorCIick(Sender: TObject): var H: HRESULT; begi n H := StdCallObject.CallError; Memol.Lines.Add(Format('StdCall.CallError HRESULT=^x'. EH])); end: Следующие три кнопки вызывают обращения клиента, поддерживающего со- глашение о вызовах stdcall, к серверу, поддерживающему соглашение о вызовах
340 Глава 7. Создание внутрипроцессных серверов автоматизации safecall. Метод Add по-прежнему возвращает S_OK, хотя явно мы этого не делали, код успешного завершения предоставлен реализацией, поддерживающей согла- шение о вызовах safecall: procedure TfStdCall.SafeCaf1AddClick(Sender: TObject): var C: Integer: H; HRESULT: begi n H := SafeCallObject.Add(5. 8, C); Memol.Lines.Add(Format('SafeCall.Add OXd, HRESULT=£x', [С. H])); end: Исключение в Call Error обрабатывается кодом Delphi. Обратите внимание на то, что Delphi подставляет другой код ошибки, нежели стаб СОМ: procedure TfStdCall.SafeCalICal1ErrorClIckCSender: TObject); var H: HRESULT; begin H := SafeCallObject.Call Error: Memol.Lines.Add(Format('SafeCall.CallError HRESULT=$x', [H])): end; И, наконец, метод BadHRESULT отрабатывает идентично методу, поддерживаю- щему соглашение о вызовах stdcall: procedure TfStdCall.SafeCallBadHRESULTClick(Sender: TObject): var H: HRESULT; begin H := SafeCallObject.BadHRESULT: Memol.Lines.Add(Format('SafeCall.BadHRESULT HRESULT=^x'. [H])): end: Второй клиент создан с библиотеками типов, импортированными с поддерж- кой соглашения safecall. Различия проявляются сразу. Во-первых, мы не получаем возвращаемого значения с кодом завершения функции. Во-вторых, исключение в функции (или значение типа HRESULT с кодом ошибки) приводит к исключению на клиенте. Рассмотрим результаты обращения к серверам подробнее. Обращения клиента, поддерживающего соглашение о вызовах safecall, к сер- веру, поддерживающему соглашение о вызовах stdcall, показывают, как идет об- работка кодов возврата при вызове safecall-методов. Метод Add, как и ранее, не вызывает проблем: procedure TfSafeCall.StdCallAddClick(Sender: TObject); var C: Integer:
Обработка ошибок 341 begin StdCallObject.Add(5. 8. С); Memol.Lines.Add(Format('StdCall.Add C=W . [C])): end: При вызове Call Error возникает исключение, сгенерированное на основе кода возврата СОМ для исключения в stdcall-методе. Как и ожидалось, СОМ ничего не знает об исключениях Delphi и не может распознать ни вида ошибки, ни сооб- щения о ней: procedure TfSafeCall.StdCal1 Cal 1ErrorCl1ck(Sender: TObject): begin StdCal1 Object.Call Error; Memol.Lines.Add('StdCall.Call Error'): end: Сказанное иллюстрирует рис. 7.7. Рис. 7.7. Результат работы клиента и сервера, поддерживающих разные соглашения о вызовах Метод BadHRESULT генерирует значение 01 eError с кодом, возвращенным из ме- тода СОМ: procedure TfSafeCall,StdCallBadHRESULTClick(Sender: TObject): begin StdCallObject.BadHRESULT: Memol.Li nes.Add('StdCal1.BadHRESULT'): end; И, наконец, посмотрим на обращения клиента, поддерживающего соглашение о вызовах safecall, к серверу, тоже поддерживающему соглашение о вызовах safecall.
342 Глава 7. Создание внутрипроцессных серверов автоматизации Интерес представляет результат, полученный при вызове CallError. На этот раз исключение обработано кодом safecall-метода, и объект сформировал полную информацию об ошибке, включая текст исключения, который корректно отобра- жается на клиенте (рис. 7.8). procedure TfSafeCall.SafeCalICal1ErrorCITckCSender: TObject); begi n SafeCal1 Object.Call Error; Memol.Lines.Add('SafeCall.CallError'); end: Рис. 7.8. Результат работы клиента и сервера, поддерживающих одинаковые соглашения о вызовах Таким образом, можно сделать следующие выводы; Ж соглашение о вызовах safecall является всего лишь оболочкой стандартной для СОМ stdcall-функции, возвращающей значение типа HRESULT, и полностью с ней совместимо; Ж можно импортировать библиотеки типов с поддержкой соглашения safe call, даже если сервер создан не в Delphi; в можно создавать сервер с safecall-методами, и с ним сможет работать любой COM-совместимый клиент; « при реализации методов сервера с поддержкой соглашения safecall Delphi ав- томатически производит всю работу по формированию кодов возврата и пере- даче на клиент совместимой с СОМ информации об ошибке; Ж при обращении к любому COM-серверу при помощи safecall-метода Delphi автоматически транслирует коды возврата СОМ в исключения;
Нотификационные сообщения 343 а рекомендуется использовать соглашение safecall, если только вам не нужен какой-то специфический контроль над генерацией или обработкой ошибок; а при реализации метода сервера с поддержкой соглашения stdcall не рекомен- дуется генерировать в нем исключений, если их нельзя обработать в рамках этого же метода. Нотификационные сообщения В главе 3 рассматривались нотификационные сообщения, передаваемые клиенту внепроцессным сервером. Ниже мы рассмотрим, как уведомить клиента о собы- тиях, происходящих во внутрипроцессном сервере автоматизации. Для COM-объектов (к которым относится и сервер автоматизации) уведом- ление клиента осуществляется посредством относительно нового интерфейса IConnecti onPoi nt. Компания Borland впервые реализовала его в Delphi 4. Однако для внутрипроцессных серверов автоматизации нотификацию легко реализовать и в более ранних версиях Delphi. Главным в идее получения нотификационных сообщений является тот факт, что внутрипроцессный сервер автоматизации работает в адресном пространстве клиента. При этом если передать указатель серверу от клиента, то этот указатель будет действительным — то есть он реально будет указывать на какой-либо объ- ект в памяти. В частности, этим объектом может быть и адрес функции, который следует вызвать для уведомления клиента. При этом надо понимать, что метод класса характеризуется двумя адресами — адресом метода и адресом данных, в то время как функция, не относящаяся к классу, характеризуется одним адресом. Создадим новую библиотеку ActiveX, выбрав значок ActiveX library на странице ActiveX окна репозитария (открывается командой File ► New ► Other), и на базе этой библиотеки создадим объект автоматизации, выбрав значок Automation object на той же странице. Определим какое-либо имя класса (например, NotelnpTest) и укажем, что сервер работает в режиме Multiple Instance. Затем к проекту добавим форму, выбрав команду File ► New form и сохранив модули под именами Uform (до- бавленная форма) и UNotelmpl (модуль реализации команд автоматизации). Сам проект сохраним под именем Notelnp. На форму поместим компонент TEdit (рис. 7.9). Рис 7.9. Форма приложения-сервера Notelmp.dll Перед описанием класса формы определим процедурный тип TtextChangeEvent. В секции public формы определим переменную этого типа и создадим обработ- чик события OnChange для компонента TEdit: unit UForm: interface
344 Глава 7. Создание внутрипроцессных серверов автоматизации uses Windows, Messages. Syslltils. Classes, Graphics. Controls, Forms. Dialogs. StdCtrls: type TTextChangeEvent=procedure(const EditText:String); stdcal1: TForm2 = class(TForm) Editl: TEdit: procedure EditlChange(Sender: TObject); private { Private declarations } public { Public declarations } FOnT extChange:TTextChangeEvent; end: implementation {$R *.DFM} procedure TForm2.EditlChange(Sender: TObject); begin if Assigned(FOnTextChange) then FOnTextChange(Editl.Text); end: end. Перейдем к редактору библиотеки типов, отметим интерфейс INotelnpTest и в его контекстном меню выберем команду New Method. Присвоим ему имя ShowDialog и укажем, что он имеет один параметр NI типа Integer. Вообще в дан- ном случае желательно иметь указатель, но указатель нельзя использовать в сер- верах автоматизации. Щелкнем на кнопке Refresh панели инструментов окна ре- дактора и в модуле реализации вызовем диалоговое окно: unit UNotelmp: interface uses ComObj. ActiveX. NoteInp_TLB: type TNotelnpTest = class(TAutoObject, INotelnpTest) protected procedure ShowDialog(NI: Integer): safecall: end:
Нотификационные сообщения 345 implementation uses ComServ. UForm. Forms: procedure TNoteInpTest.ShowDialog(NI: Integer): var FD:TForm2: begin FD := nil: try FD := TForm2.Create(nil); FD.FOnTextChange := TTextChangeEvent(NI); FD.ShowModal; finally if Assigned(FD) then FD.Release: Appl i cati on.ProcessMessages: end; end: initialization TAutoObjectFactory.CreateCComServer. TNotelnpTest. Class_NoteInpTest. ciMultiInstance): end. Диалоговое окно вызывается в защищенном блоке try...finally...end, так как необходимо вызвать его деструктор. Вызов Application.ProcessMessages желате- лен для гарантированного вызова деструктора — иначе DLL может выгрузиться раньше его отработки. После компиляции и регистрации сервера выбором команды Run ► Register ActiveX server завершим проект. Теперь необходимо создать клиентское приложение для тестирования данного сервера. Создадим новый проект выбором команды File ► New Application и помес- тим на форму кнопку с заголовком Create Dialog (рис. 7.10). Г Forml ЙЯЕ • Cieete Рис. 7.10. Главная форма клиентского приложения, получающего уведомления от сервера Notelmp.dll Далее создадим метод HookNotify, который не относится ни к какому классу и имеет директиву вызова stdcall. Метод включает параметр const S:String и вы- водит эту строку в заголовок главной формы приложения. В обработчике собы- тия OnCI i ck кнопки создадим COM-объект и вызовем его экспонируемый метод
346 Глава 7. Создание внутрипроцессных серверов автоматизации ShowDialog. В качестве параметра передадим адрес метода HookNotify, приведен- ный к целому числу. unit UNoteCll: interface uses Windows. Messages, SysUtils. Classes. Graphics. Controls, Forms. Dialogs. StdCtrls, ComObj, Variants: type TForml = class(TForm) Buttonl: TButton: procedure ButtonlClick(Sender: TObject): private V: Variant: end: var Forml: TForml; implementation procedure HoOKNotify(const S: String); stdcall: begin Forml.Caption := S; end: procedure TForml.ButtonlClick(Sender: TObject): begin V := CreateOleObject('Notelnp.NotelnpTest'): V.ShowDi a1og(Integer(OHoOKNot1fy)): V := Unassigned: end: end. После компиляции и запуска проекта при щелчке на кнопке появляется диа- логовое окно сервера. Если в этом окне изменить содержимое компонента TEdit, то об этом немедленно будет сообщено клиенту, и новое значение текста появится на заголовке клиентской формы (рис. 7.11). Рис. 7.11. Получение клиентом нотификационного сообщения от сервера Notelmp.dll
Нотификационные сообщения 347 Теперь посмотрим, как вызвать метод класса. Для этого сначала слегка моди- фицируем сервер. В модуле UForm определим процедурный тип: TTextClassChangeEvent = procedure!const S: String) of object: stdcal1: В секции public класса TForm2 определим переменную этого типа: FOnTextClassChange: TTextClassChangeEvent: Далее, в обработчик события Edi tlOnChange добавим вызов этого метода: if Assigned(FOnTextClassChange) then FOnTextClassChange(Editl.Text): Теперь необходимо перейти к редактору библиотеки типов и добавить метод ShowClassDialog к интерфейсу INotelnpTest. Метод вызывается с двумя параметрами типа Integer: Code и Data. После щелчка на кнопке Refresh реализуем этот метод в модуле UNotelmp следующим образом: procedure TNotelnpTest.ShowClassDialogICode. Data: Integer): var FD: TForm2; begin FD := nil; try FD := TForm2.Create(nil); TMethod!FD.FOnTextClassChange).Code := Pointer(Code): TMethod!FD.FOnTextClassChange).Data := Pointer(Data): FD.ShowModal: finally FD.Release: Appli cation.ProcessMessages: end; end; Структура TMethod определена в модуле SysUtils, поэтому его надо добавить в секцию uses. На этом редактирование серверной части можно считать завер- шенной. В клиентской части (проект NotelnpCl ient) необходимо сделать следующие изменения. В секции private класса TForml объявляется процедура: procedure HoOKClassNotify(const S: String): stdcall; Также нужно написать ее реализацию: procedure TForml.HoOKClassNotifyfconst S: String): begin Caption := S: end: Далее, определим новый процедурный тип: THoOKClassNotify = procedure!const S: String) of object: stdcall;
348 Глава 7. Создание внутрипроцессных серверов автоматизации Добавим еще одну кнопку к форме и в обработчике события этой кнопки OnCl 1 ck напишем следующий код: procedure TForml.Button2Click(Sender: TObject): var Method: THoOKClassNotify: begin Method := HoOKClassNotify: V:=Create01e0bject('Notelnp.NotelnpTest'); V.ShowClassDi alog(Integer(TMethod(Method).Code). Integer(TMethod(Method).Data)): V:=Unassigned: end: Промежуточную переменную Method (и, соответственно, тип этой переменной) необходимо описывать, поскольку компилятор при попытке скомпилировать следующую строку останавливается с диагностикой, что не хватает параметров после HookClassNotify: Integer(TMethod(HoOKClassNoti fy).Code) После компиляции проекта можно щелкнуть на любой из двух кнопок — в обоих случаях появится диалоговое окно сервера автоматизации. При изменении данных в компоненте TEdit все изменения немедленно отражаются в заголовке формы клиента. Из реализации методов видно, что в одном случае клиент получает сооб- щения через метод класса, который можно вызвать, не создавая экземпляра этого класса, а в другом случае — через функцию, не имеющую отношения к классу (рис. 7.12). Рис. 7.12. Получение сообщения с помощью метода класса Таким образом, мы видим, что реализация СОМ-сервера в виде DLL позво- ляет значительно упростить проект за счет применения общего адресного про- странства, и в первую очередь это касается реализации нотификационных со- общений, когда СОМ-сервер вызывает функции, реализованные в клиенте. Для этого достаточно в СОМ-сервер передать адрес вызываемой функции. Сравните это с примерами из главы 3, в которой описывалось создание нотификационных сообщений для сервера, реализованного в виде исполняемого файла. Напомним, что при этом упрощается и передача данных — их можно передавать через указа- тели, приведенные к типу Integer. Это особенно важно при работе с большими объемами данных, требующими значительных ресурсов.
Заключение 349 Заключение В настоящей главе мы обсудили вопросы создания динамически загружаемых библиотек и серверов автоматизации, выполненных в виде DLL. Мы узнали, что применение DLL обладает рядом преимуществ, таких как: В одновременное обслуживание несколько приложений; Я возможность хранения общих ресурсов; Я поддержка новых версий приложений; S возможность использования разных языков программирования для создания различных частей приложений; В экономия ресурсов за счет динамической загрузки. Мы обсудили вопросы применения DLL, соглашения о вызовах функций DLL, особенности статической и динамической загрузки DLL, обмен данными с DLL, вызов функций приложения в DLL, работу с объектами в DLL, хранение мо- дальных и немодальных форм в DLL, экспорт дочерних форм из DLL. Мы изучили принципы создания внутрипроцессных серверов автоматизации (серверов, реализованных в виде DLL), в частности их создание и регистрацию. Мы также уделили внимание обработке ошибок во внутрипроцессных серверах автоматизации, а также соглашениям о вызовах stdcall и safecall. И наконец, мы обсудили реализацию нотификационных сообщений в серверах автоматизации. Изучив создание DLL и внутрипроцессных серверов автоматизации, мы убе- дились в том, что реализация COM-сервера в виде DLL позволяет значительно упростить проект за счет применения общего адресного пространства, и в пер- вую очередь это касается реализации нотификационных сообщений. Отметим, что при реализации COM-сервера в виде DLL упрощается и передача данных между клиентом и сервером, так как в этом случае можно использовать указатели, приведенные к типу Integer, что особенно важно при передаче больших объемов данных. Одним из наиболее интересных применений внутрипроцессных серверов яв- ляется создание модулей расширения Microsoft Office. Именно об этом мы и по- говорим в следующей главе.
ГЛАВА 8 Создание модулей расширения Microsoft Office В предыдущей главе мы начали разговор о создании COM-серверов, реализован- ных в виде DLL. Одним из наиболее интересных применений таких серверов яв- ляется, на наш взгляд, создание модулей расширения Microsoft Office. Решения подобного рода нередко применяются в компаниях, в которых основная работа пользователей происходит именно с офисными приложениями — это позволяет экономить немало средств на обучении персонала. Модель модулей расширения Microsoft Office 2000 Начиная с Office 2000, компания Microsoft ввела единый API для создания моду- лей, расширяющих функциональные возможности пакета. Теперь можно писать модули, которые интегрируются с любым из приложений пакета. Разработанный набор интерфейсов упрощает создание расширений, позволяя программисту сконцентрироваться на функциональности, вместо того чтобы изучать особенно- сти каждого из приложений. Новая модель модулей расширения была названа COM Add-Ins. Как следует из названия (add-in), модуль расширения, или надстройка, представляет собой COM-сервер, который специальным образом зарегистрирован в операционной системе. При старте приложение Office загружает зарегистрированные модули расширения и вызывает их методы, позволяя настроить обработчики на необхо- димые для их дальнейшего функционирования события. По завершении работы приложение Office извещает об этом модули расширения, позволяя последним корректно освободить ресурсы. Общение модулей расширения с загрузившим их приложением происходит посредством объектной модели Microsoft Office. Использование модулей расширения COM Add-Ins позволяет в корне изме- нить подходы к интеграции приложений с Microsoft Office. Например, вместо того чтобы вызывать Word из приложения для генерации отчетов, можно, на- оборот, встроить в Word меню со списком отчетов, а по выбору пользователем нужного отчета запросить данные из базы данных и сформировать по ним до- кумент.
Интерфейс IDTExtensibility2 351 Интерфейс IDTExtensibility2 Ключевым моментом в написании модуля расширения COM Add-In является реализация им интерфейса IDTExtensi bi 11 ty2, определенного следующим образом: type IDTExtensibi 1ity2 = interface(IDispatch) [’{B65AD801-ABAF-11D0-BB8B-00A0C90F2744}’] procedure OnConnection(const HostApp: IDispatch: ext_ConnectMode: Integer: const Addlnlnst: IDispatch; var custom: PSafeArray): safecall: procedure OnDisconnection(ext_DisconnectMode: Integer: var custom: PSafeArray); safecall: procedure OnAddInsUpdate(var custom: PSafeArray); safecall: procedure OnStartupComplete(var custom: PSafeArray); safecall; procedure BeginShutdown(var custom: PSafeArray); safecall; end; После загрузки зарегистрированного в качестве модуля расширения СОМ- сервера приложение Office запрашивает у него интерфейс IDTExtensibi 1 ity2 и, если он реализован, вызывает соответствующие методы, позволяя модулю расшире- ния реализовать свою функциональность. Рассмотрим методы интерфейса IDTExtensibi 1 ity2 подробнее. Первый из рас- сматриваемых методов — OnConnecti on: procedure OnConnection( const HostApp: IDispatch: ext_ConnectMode: Integer: const Addlnlnst: IDispatch: var custom: PSafeArray ); safecal1; Этот метод вызывается при загрузке модуля расширения. В этот момент можно произвести требуемую инициализацию, добавить или удалить необходимые ин- терфейсные элементы, установить обработчики событий и т. п. Параметры, передаваемые методу OnConnecti on, перечислены ниже. И HostApp — ссылка на интерфейс IDispatch вызывающего приложения. Если планируется в дальнейшем из модуля расширения обращаться к объектной модели приложения, модуль расширения должен сохранить ссылку в пере- менной. И ext_ConnectMode — константа, идентифицирующая причину загрузки: □ ext_cm_AfterStartup — модуль расширения загружен после загрузки хотя бы одного документа Office; □ ext_cm_External — модуль расширения загружен другим компонентом;
352 Глава 8. Создание модулей расширения Microsoft Office □ ext_cm_Startup — модуль расширения загружен до загрузки хотя бы одного документа Office; □ ext_cm_CommandLine — в Office 2000 не используется. » Addlnlnst — ссылка на интерфейс IDispatch загружаемого модуля расширения. » custom — в Microsoft Office не используется (в других методах интерфейса IDTExtensiЫ11 ty2 этот параметр также не используется). Следующий метод интерфейса IDTExtensiЫ11ty2, который мы рассмотрим, — OnDisconnection: procedure 0nD1sconnection(ext_DisconnectMode: Integer: var custom: PSafeArray); safecall: Этот метод вызывается при выгрузке модуля расширения. Он должен освобо- дить занятые ресурсы и выполнить прочие процедуры, связанные с завершением работы СОМ-сервера. Параметр ext_Di sconnectMode информирует о причине выгрузки СОМ-сервера и может принимать следующие значения: « ext_dm_HostShutdown — приложение завершает работу; ® ext_dm_UserC1osed — модуль расширения выгружен пользователем. Помимо рассмотренных выше методов, интерфейс IDTExtensibi 11 ty2 также об- ладает методами OnAddlnsUpdate, OnStartupComplete и Begi nShutdown: procedure OnAddlnsUpdatelvar custom: PSafeArray): safecall: Вызывается при изменении списка загруженных модулей расширений. СОМ- сервер может проанализировать коллекцию расширений приложения и пред- принять необходимые действия, требующие взаимодействия с другими модулями расширения. procedure OnStartupComplete(var custom: PSafeArray); safecall; Вызывается по завершении инициализации приложения. Те модули расши- рения, которые должны предоставлять интерфейс пользователя при запуске приложения, должны делать это в методе OnStartupComplete, когда приложение полностью завершило этап инициализации. procedure BeginShutdown(var custom: PSafeArray): safecall; Вызывается в начале процесса завершения приложения, позволяя модулям расширения предпринять в этот момент какие-либо действия. Внедрение в объектную модель Office Как было показано в предыдущем разделе, модулю расширения доступен интерфейс IDispatch вызывающего приложения. Таким образом, можно модифицировать интерфейс этого приложения, добавляя и удаляя необходимые интерфейсные элементы, создавать документы, вызывать различные методы для работы с ними.
События COM 353 Однако, как правило, также необходимо обеспечить реакцию на различные собы- тия в вызывающем приложении (такие как изменение документа или щелчок на кнопке панели инструментов). Для этого модуль расширения должен установить обработчик соответствующего события. Прежде чем перейти к вопросам реализа- ции, совершим небольшой экскурс по событиям СОМ и их обработчикам. События СОМ Если COM-объект хочет получать информацию о событиях в другом СОМ-объ- екте, то он должен уведомить об этом объект-источник событий, зарегистрировав себя в списке объектов-получателей уведомлений о событиях. Модель СОМ пре- доставляет для этого стандартный механизм. Объект-источник событий (в нашем случае — приложение Office, кнопка на пане- ли инструментов, документ и т. п.) реализует интерфейс IConnectionPointContainer. Объект, нуждающийся в оповещении о событиях, должен запросить у источника этот интерфейс, затем при помощи метода FindConnectionPoint получить «точку подключения» — интерфейс IConnectionPoint и посредством вызова метода Advise зарегистрировать в этой точке подключения ссылку на свою реализацию интер- фейса IDispatch, методы которого будут вызываться при возникновении тех или иных событий в их источнике. Различные объекты Office определяют интерфейсы, которым должны соответ- ствовать обработчики их событий. Так, например, для объекта CommandBarButton (кнопка на панели инструментов) определен интерфейс обработчика: type _CommandBarButtonEvents = dispinterface ['{000С0351-0000-GOOD-С000-000000000046}] procedure Clicklconst Ctrl: CommandBarButton: var Cancel Default: WordBool): dispid 1: end: Это означает, что при щелчке на кнопке будет вызван метод Invoke зарегист- рированного интерфейса-обработчика с параметром Dispid, равным 1. При этом параметр pDispIds будет содержать указатель на вариантный массив из двух па- раметров — соответственно типов CommandBarButton и WordBool. Базовый класс обработчика СОМ-событий Реализуем базовый класс обработчика COM-событий. Реализация взята из модуля SinkObject.pas (адрес источника — http://www.techvanguards.com), написанного Бином Ли (Binh Ly). type TBaseSink = classITObject. IUnknown. IDispatch) Protected { методы IUnknown } function Querylnterfacelconst IID: TGUID: out Obj): HResult: stdcall;
354 Глава 8. Создание модулей расширения Microsoft Office function _AddRef: Integer: stdcall; function _Release: Integer: stdcall: { методы IDispatch } function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocalelD: Integer: DispIDs: Pointer): HResult: vi rtual: stdcal1; function GetTypeInfo(Index. LocalelD: Integer; out Typeinfo): HResult: virtual: stdcall: function GetTypeInfoCount(out Count: Integer): HResult: virtual: stdcall: function InvOKe(DispID: Integer: const IID: TGUID: LocalelD: Integer: Flags: Word: var Params: VarResult. Exceplnfo, ArgErr: Pointer): HResult: virtual: stdcall: private FCoOKie: Integer; FCP: IConnectionPoint; FSinkllD: TGUID; FSource: IUnknown: function DoInvOKe(DispID: Integer: const IID: TGUID: LocalelD: Integer: Flags: Word; var dps: DispParams: pDispIds: PDispIdList; VarResult. Exceplnfo. ArgErr: Pointer): HResult: virtual; abstract: public destructor Destroy; override: procedure Connect(pSource : IUnknown): procedure Disconnect: property SinkllD: TGuid read FSinkllD; property Source: IUnknown read FSource: end: Рассмотрим реализацию ключевых методов этого класса. Метод Queryinterface в дополнение к стандартной реализации проверяет, нет ли попыток запросить интерфейс обработчика событий. В этом случае возвращается интерфейс I Di spatch, позволяющий объекту-источнику событий вызвать метод Invoke: function TBaseSink.Querylnterfacelconst IID: TGUID: out Obj): HResult; begin Result := E_NOINTERFACE; Pointer(Obj) := nil: if GetInterfacedID, Obj) then Result := S_OK: // если запрашивается интерфейс SinkllD. 11 возвращаем свой интерфейс IDispatch if not Succeeded(Result) then if IsEqualIID(IID. FSinkllD) then
События COM 355 if Getlnterface!IDispatch, Obj) then Result := S_OK: end; Метод Connect регистрирует COM-объект в качестве обработчика событий COM-объекта pSource. Обратите внимание, что переменная FCP объявлена как поле класса, поскольку класс должен удерживать счетчик ссылок на «точку подключения», пока она используется. Если объявить FCP как локальную пере- менную, по завершении метода Connect произойдет неявный вызов FCP._Release, что приведет к неправильной работе модуля расширения. procedure TBaseSink.Connect(pSource: IUnknown); var pcpc; IConnectionPointContainer: begin Disconnect: try // Запрашиваем интерфейс IConnectionPointContainer Ну объекта-источника событий 01 eCheck(pSource.QueryInterface!IConnect!onPoi ntContai ner. pcpc)): // Запрашиваем интерфейс IConnectionPoint 01 eCheck(pcpc.FindConnectionPoint(FSinkllD. FCP)); // Подключаемся к обработчику событий OleCheck(FCP.Advise(Self, FCoOKie)): // Все прошло успешно - устанавливаем свойство Source FSource := pSource; except raise Exception.Create! Format('Unable to connect ^s.'#13'Xs'. [ClassName, Exception(ExceptObject).Message])); end; end: Метод Di sconnect отключает обработчик событий от объекта-источника: procedure TBaseSi nk.Di sconnect: begin if FSource = nil then Exit; try 01 eCheck(FCP.Unadvise!FCoOKie)); FCP := nil: FSource := nil; except Pointer(FCP) := nil; Pointer(FSource) := nil; end; end:
356 Глава 8. Создание модулей расширения Microsoft Office Метод Invoke вызывается при возникновении события в объекте-источнике. Он осуществляет предварительную обработку параметров и вызывает абстракт- ный метод Dolnvoke, который должен быть перекрыт в наследниках, реализую- щих конкретные интерфейсы обработчиков событий. Реализация такого наслед- ника будет рассмотрена ниже. Обработчик событий объекта CommandBarButton Наследуя функциональность от базового класса TBaseSink, обработчики событий конкретных СОМ-объектов реализуются перекрытием методов Create и Dolnvoke. Создадим такой обработчик для кнопки панели инструментов Office. Он должен реализовать интерфейс _CommandBarButtonEvents: type _CommandBarButtonEvents = dispinterface [{000C0351-0000-0000-C000-000000000046}] procedure Click(const Ctrl: CommandBarButton: var Cancel Default: WordBool): dispid 1; end: Объявим класс: type // Обработчик события щелчка на кнопке TOnCommandButtonClick = procedure (Button: CommandBarButton: var Cancel Default: WordBool) of object: TCommandButtonEventSink = class(TBaseSink) private FOnCHck: TOnCommandButtonClick: protected procedure DoClick(Button: CommandBarButton: var Cancel Default: WordBool): virtual: function DoInvOKe (DispID: Integer: const IID: TGUID; LocalelD: Integer; Flags: Word: var dps : TDispParams: pDispIds : PDispIdList: VarResult. Exceplnfo. ArgErr: Pointer): HResult: override: public constructor Create; virtual: property OnClick: TOnCommandButtonClick read FOnClick write FOnCHck: end: В конструкторе установим идентификатор интерфейса обработчика событий, который мы реализуем: constructor TCommandButtonEventSi nk.Create: begin inherited:
Регистрация модулей расширения 357 FSinkllD := _CommandBarButtonEvents: end; Метод DoClick просто вызывает назначенный классу обработчик события и ну- жен для более удобной работы с ним из Delphi: procedure TCommandButtonEventSi nk.DoCli ck( Button: CommandBarButton: var Cancel Default: WordBool): begin if Assigned(FOnClick) then FOnClickCButton, CancelDefault): end: Ключевым является метод Dolnvoke, который для каждого параметра Displd, объявленного в интерфейсе _CommandBarButtonEvents, должен выполнить соответ- ствующие действия: function TCommandButtonEventSink.DoInvOKe(DispID: Integer: const IID: TGUID: LocalelD: Integer; Flags: Word; var dps: TDispParams: pDispIds: PDispIdList; VarResult, Exceplnfo, ArgErr: Pointer): HResult; Begin Result := S_OK: case DispID of // Для этого значения Displd передаются 2 параметра - 11 CommandBarButton и WordBool 1 : DoClick(IUnknown(dps,rgvarg*[pDispIds^[0]].unkval) as CommandBarButton. dps,rgvarg*[pDispIds*[l]].pbool*): el se Result := DISP_E_MEMBERNOTFOUND; end: end; Как видим, реализация конкретного обработчика является практически меха- нической задачей и не должна вызвать проблем. Если в интерфейсе предусмот- рено несколько методов, то следует подставить в оператор case все их параметры Displd. От программиста требуется лишь аккуратность при отображении массива dps на параметры соответствующих обработчиков. Регистрация модулей расширения Модуль расширения COM Add-Ins — это СОМ-сервер, который должен быть за- регистрирован в системе, например, при помощи утилиты TRegSvr или RegSvr32. Однако требуется еще один шаг — регистрация его в Microsoft Office. Для этого необходимо создать в реестре следующий раздел (рис. 8.1): HKEY_CURRENT_USER\Software\Microsoft\Office \<Имя npnno)neHHfl>\AddIns\<HMfl>
358 Глава 8. Создание модулей расширения Microsoft Office Рис. 8.1. Регистрация модуля расширения Microsoft Office в реестре Здесь: ж <Имя приложения> — название приложения, к которому подключается модуль расширения; в <Имя> — имя, под которым зарегистрирован СОМ-сервер в формате: название_проекта.имя_класса В этом разделе необходимо создать два параметра: Ш FriendlyName — строковый параметр, определяющий имя, под которым наш модуль расширения будет представлен в менеджере расширений приложений Microsoft Office; И LoadBehavior — параметр типа DWORD, определяющий, когда должен загружаться модуль расширения. Параметр LoadBehavior может принимать одно из следующих значений: Я 3 — модуль расширения загружается при старте приложения; Я 9 — модуль расширения загружается по требованию (когда его свойство Connected в коллекции Addins приложения установлено равным True); Я 16 — модуль расширения загружается один раз при следующем запуске при- ложения. Нередко для упрощения процедуры поставки модулей расширения перепи- сывается процедура регистрации фабрики классов. Пример такой процедуры бу- дет приведен ниже. Разработка модуля расширения Библиотеки типов Office 2000 Для работы с объектной моделью Microsoft Office 2000 нам понадобятся библио- теки типов, описывающие доступные интерфейсы. Если вы используете версию Delphi 5, которая поставляется с библиотеками типов для Office 97, необходимо
Разработка модуля расширения 359 импортировать нужные модули. В любом случае понадобится библиотека Office_ TLB, остальные (Word_TLB, Excel _TLB и т. п.) могут потребоваться в зависимости от того, для какого приложения предназначается модуль расширения (рис. 8.2). Вегт^л >(DELPHI)\Lib;$(DELPHI)\Bin;$(DELPHI)\lmport Cre—г Unit Г Generate СоппрспаЫЛпчррег Microsoft Office Euro Converter Object Library (Version 1.0) Microsoft Office Server Extensions 1.0 Object Library (Version 1,0)j Microsoft Office UA Control 1.0 TypeLib (Version 1.0) Microsoft Office Web Components 9.0 (Version 1.0) Import Type TCommandBars TCommandBarComboBox TCommandBarButton [Microsoft MIMEEDITType Library 1.0 (Version 1.0) Microsoft MultiMedia DTCs (Version 1.0) Microsoft NetShow Player (Version 1.0)_________ Import Type Library Microsoft Office 9 0 Object Library (Version 211 Рис. 8.2. Импорт библиотеки типов Microsoft Office Для открытия окна импорта нужно воспользоваться командой Project ► Import Type Library. После создания модулей с описаниями интерфейсов рекомендуем вручную удалить из них ссылки на модули Graphics, OleServer и 01 eCtrls, которые приводят к подключению к проекту модуля Forms и при компиляции не нужны. Можно пойти другим путем — в окне импорта снять флажок Generate Component Wrapper. СОВЕТ ----------------------------------------------------------------- В составе Delphi 7 поставляются интерфейсные файлы для Office ХР. Поскольку вся требуемая функциональность имеется в Office 2000, лучше в проекте использо- вать файлы от Office 2000. Тогда наше расширение будет гарантировано работать как с Office 2000, так (в силу того, что интерфейс — неизменный контракт) и со следую- щей версией Office ХР.
360 Глава 8. Создание модулей расширения Microsoft Office Создание СОМ-сервера Поскольку модуль расширения СОМ является COM-сервером, для его создания воспользуемся мастерами Delphi. Выберем команду New ► Other, в окне репозита- рия перейдем на страницу ActiveX и выберем значок ActiveX Library, чтобы создать новую библиотеку ActiveX. Затем добавим в созданную библиотеку объект авто- матизации, выбрав на той же странице значок Automation Object. В поле Class Name открывшегося окна мастера введем имя реализуемого ин- терфейса (DTExtensibility2). В принципе можно ввести любое имя, требуется только, чтобы реализуемый интерфейс имел тот же идентификатор GUID, что и интерфейс IDTExtensi bi lity, а также аналогичный набор методов. После того как Delphi создаст новый объект автоматизации, запустим редак- тор библиотеки типов. Вначале на вкладке Text окна редактора представлено описание интерфейса без методов и со сгенерированным Delphi значением GUID (рис. 8.3). Рис. 8.3. Библиотека типов созданного объекта автоматизации Вместо сгенерированных Delphi значений введем там следующий текст: [ uuid(B65AD801-ABAF-HD0-BB8B-00A0C90F2744), version(l.O). helpstring( "Dispatch interface for 0ffice2000CornAddIn Object"), dual. ol eautomation 1 interface IDTExtensibility2: IDispatch
Разработка модуля расширения 361 Cid(OxOOOOOOOl)] HRESULT _stdcall OnConnection( [In] IDispatch * HostApp. [in] long ext_ConnectMode. [in] IDispatch * Addlnlnst. [in] SAFEARRAY(VARIANT) * custom ); [id(0x00000002)] HRESULT _stdcall OnDisconnection( [in] long ext_DisconnectMode. [in] SAFEARRAY(VARIANT) * custom ); [id(0x00000003)] HRESULT _stdcall OnAddInsUpdate( [in] SAFEARRAY(VARIANT) * custom ); [id(0x00000004)] HRESULT _stdcall OnStartupComplete( [in] SAFEARRAY(VARIANT) * custom ); [id(0x00000005)] HRESULT _stdcall BeginShutdown( [in] SAFEARRAY(VARIANT) * custom ): }: Если название вашего класса — не DTExtensi bi 1 i ty2, следует скорректировать название интерфейса. Все остальное, включая GUID, должно быть введено точно так же, как описано ранее. Если все введено правильно, в окне Type Library Editor должны появиться пять методов созданного интерфейса (рис. 8.4). Рис. 8.4. Описание методов созданного интерфейса
362 Глава 8. Создание модулей расширения Microsoft Office Щелкнем на кнопке Refresh панели инструментов и закроем окно редактора библиотеки типов — больше он нам не понадобится. Теперь откомпилируем полученный проект и зарегистрируем его в реестре Windows, выбрав команду Run ► Register COM Server. При помощи редактора реестра создадим в реестре Windows запись для реги- страции модуля расширения с приложением Microsoft Office (можно также пере- писать процедуру регистрации СОМ-сервера — мы сделаем это в конце следую- щего раздела). Модуль расширения СОМ готов! Отладка модулей расширения Для работы с модулями расширения необходимо добавить в меню Microsoft Office команду для вызова диспетчера надстроек. Выполним эту процедуру на примере Word. 1. В меню Сервис выберем команду Настройка и в открывшемся диалоговом окне перейдем на вкладку Команды. 2. В списке Категории выберем категорию Сервис. 3. Перетащим команду Надстройки для модели СОМ из списка Команды в нуж- ное место меню Сервис. 4. Щелкнем на кнопке Закрыть. При помощи новой команды вызывается окно диспетчера надстроек. Загру- женные надстройки помечены в списке надстроек этого окна флажками. Чтобы загрузить надстройку, установим соответствующий флажок и щелкнем на кноп- ке ОК, чтобы выгрузить — снимем флажок. ВНИМАНИЕ --------------------------------------------------------- Если во время загрузки модуля расширения происходит ошибка, при следующем за- пуске приложения модуль не будет загружаться автоматически. Чтобы надстройка снова начала загружаться, следует загрузить ее через диспетчер (установив соответст- вующий флажок). Для отладки модулей расширения откроем диалоговое окно Parameters (ко- мандой Run ► Parameters) и в поле Host Application введем (или выберем, щелкнув на кнопке Browse) имя приложения Office, к которому подключена надстройка, например WinWord.exe. После этого установим точку прерывания в одном из ме- тодов своего объекта и запустим приложение. Загрузится Word, и при попада- нии на точку прерывания мы окажемся в отладчике Delphi. Реализация функциональности В настоящий момент наш модуль расширения умеет только загружаться, не де- лая ничего полезного. Добавим на панель инструментов Office кнопку, при щелчке на которой в текущую позицию курсора будет вставляться список файлов вы- бранного каталога.
Разработка модуля расширения 363 Для этого дополним наш объект автоматизации несколькими полями и мето- дами: type TDirectoryList = class(TAutoObject, IDTExtensibi 11ty2) private Host: WordApplication: FButtonEventsSi nk: TCommandButtonEventSi nk; procedure ButtonClick(Button: CommandBarButton: var Cancel Default: WordBool): protected // Реализация IDTExtensi bi Hty2 procedure BeginShutdown(var custom: PSafeArray); safecall: procedure OnAddInsUpdate(var custom: PSafeArray): safecall; procedure OnConnection(const HostApp: IDispatch; ext_ConnectMode: Integer; const Addlnlnst; IDispatch: var custom: PSafeArray): safecall; procedure OnDisconnection(ext_DisconnectMode; Integer; var custom: PSafeArray); safecall; procedure OnStartupComplete(var custom: PSafeArray): safecall: end: Поле Host нашего объекта будет хранить ссылку на интерфейс WordAppl 1 cation, необходимый для работы с объектной моделью Word, поле FButtonEventsSi nk — ссылку на объект-обработчик событий от кнопки, реализация которого была рассмотрена в подразделе «Обработчик событий объекта CommandBarButton» раздела «События СОМ», а метод ButtonClick будет вызываться для обработки щелчков на кнопке. Реализуем необходимую функциональность в методах класса TDi rectoryLi st: const // Уникальный идентификатор кнопки. Можно И задать любую уникальную строку. Для ее генерации // удобно воспользоваться средствами // Delphi для генерации GUID (клавиши Ctrl+Shift+G). И Этот идентификатор понадобится нам для того. И чтобы после создания кнопки иметь возможность ее найти. BUTTONJAG = '{1A1552DC-9286-11D3-BCA0-00902759A497}'; procedure TDi rectoryLi st.OnConnection( const HostApp: IDispatch: extJonnectMode: Integer: const Addlnlnst; IDispatch: var custom: PSafeArray); var Bar: CommandBar; Button: CommandBarButton: begin // Сохраняем ссылку на WordApplication для И последующей работы с этой ссылкой
364 Глава 8. Создание модулей расширения Microsoft Office Host := HostApp as WordApplication; // Создаем обработчик событий для кнопки FButtonEventsSink := TCommandButtonEventSink.Create: FButtonEventsSink.OnClick := ButtonClick: // Получаем интерфейс панели И инструментов "Форматирование" Bar := Host.CommandBars.Get_Item('Formatting'); ' // Проверяем наличие на панели инструментов нашей кнопки Button := Bar.FindControl(msoControlButton. EmptyParam. B(JTTON_TAG. EmptyParam. msoFalse) as CommandBarButton: if not Assigned(Button) then 11 Если ее нет - создаем Button := Bar.Controls.Add(msoControlButton. EmptyParam. B(JTTON_TAG. 1. EmptyParam) as CommandBarButton; // Подключаем обработчик и устанавливаем свойства кнопки FButtonEventsSink.Connect(Button); Button.Set_Sty1e(msoButtonCapti on); Button. Set_Tag (B(JTTON_TAG); Button.Set_Capti on('Di r') end; procedure TDi rectoryLi st.OnDi sconnecti on( ext_DisconnectMode: Integer; var custom: PSafeArray): var Bar; CommandBar; B: CommandBarControl: begin // Уничтожаем обработчик событий кнопки F reeAndNi1(FButtonEventsSi nk); // Ищем свою кнопку Bar := Host.CommandBars.Get_Item('Formatting'); В ;= Bar.FindControl(msoControlButton. EmptyParam. BUTTON_TAG, EmptyParam. msoFalse) as CommandBarButton; // И удаляем ее if Ass?gned(B) then B.Delete(msoFalse); end; procedure TDi rectoryLi st.ButtonCli ck( Button: CommandBarButton: var Cancel Default: WordBool); var S: String: SR: TSearchRec; D: WordDocument: FindStatus: Integer; Begi n // Эта процедура вызывается при щелчке
Разработка модуля расширения 365 // на созданной нами кнопке О := Host.ActiveDocument: // Проверяем наличие активного документа if Assigned(D) then begin // Функция BrowseForFolder возвращает путь к выбранному И в диалоговом окне каталогу. Код функции приведен в модуле // с примером на прилагаемом к книге компакт-диске. И От использования аналогичной функции VCL // SeiectDirectory, пришлось отказаться, так как И модуль, в котором она находится, использует Forms if BrowseForFolder(S) then begin with TStringList.Create do try // Получаем список файлов FindStatus := FindFirstCS + 0. SR); while FindStatus = 0 do begin Add(SR.Name); FindStatus := FindNext(SR): end; FindClose(SR); // И вставляем его в документ 0. ActiveWindow.Selection.InsertAfter(S+#13#13+Text); finally // Освобождаем TStringList Free; end; end; end; end; И, наконец, перепишем процедуру регистрации нашего модуля расширения: type TDirectoryListFactory=class(TAutoObjectFactory) public procedure UpdateRegistry(Register: Boolean): override; end; { TDirectoryListFactory } procedure TDirectoryListFactory.UpdateRegistry( Register: Boolean): var Reg:TRegistry; Section: String; begin Reg;=nil; Section := ’Software\Microsoft\Office\Word\Addins\' + ’SimpleAddIn.DTExtensibility2'; try Reg:=TRegi stry.Create:
366 Глава 8. Создание модулей расширения Microsoft Office Reg.RootKey:=HKEY_CURRENT_USER; if Register then begin inherited UpdateRegistry(Register); Reg.OpenKey(Section.True); Reg.WriteStringt'FrlendlyName'. 'Test Directory List Addin'): Reg.WriteInteger('LoadBehavior'.3); end else begin Reg.DeleteKeytSection): i nheri ted UpdateRegi stry(Regi ster); end: finally Reg.Free: end: end; initialization TDi rectoryListFactory.Create(ComServer, TDIrectoryList. Class_DTExtensibi 11ty2, clliultiInstance. tmApartment): Следует обратить внимание на то, что теперь в секции initialization создается экземпляр класса TDI rectoryLi stFactory, а не TAutoObjectFactory. Пример работы этой надстройки иллюстрирует рис. 8.5. Рис. 8.5. Результат работы модуля расширения Microsoft Office
Создание смарт-тегов для Office ХР 367 Написание надстроек, работающих с несколькими приложениями Office Поскольку все приложения Office реализуют одну и ту же модель COM Add-Ins, то один и тот же модуль расширения может быть зарегистрирован одновременно для нескольких приложений. В этом случае он должен уметь определять, из какого приложения загружен, и использовать соответствующую объектную модель. Определить приложение-владельца можно, запросив у него соответствующий интерфейс: procedure TDIrectoryList.OnConnectiont const HostApp: IDispatch: ext_ConnectMode: Integer; const Addlnlnst: IDispatch; var custom: PSafeArray); begin if HostApp is WordApplication then // Это Microsoft Word Также возможно сохранение параметра HostApp в переменной типа Variant и использование позднего связывания. В этом случае надстройка будет работать с любым приложением Office, имеющим подходящие по именам методы. Создание смарт-тегов для Office ХР Еще одним вариантом реализации собственных модулей расширения Microsoft Office являются смарт-теги, появившиеся впервые в Microsoft Office ХР. Смарт- теги представляют собой довольно интересное новшество, и при желании им можно найти массу полезных применений. Ниже мы выясним, как с помощью Delphi создавать библиотеки DLL, реализующие смарт-теги для Office ХР, и как осуществить их поставку конечным пользователям. Но прежде обсудим, что они собой представляют. Понятие смарт-тегов Смарт-теги (smart tags) представляют собой элементы пользовательского интер- фейса Microsoft Word 2Q02, Microsoft Excel 2002, Microsoft Outlook 2002 (если в качестве редактора электронных сообщений используется Word 2002) и Micro- soft Internet Explorer 6 (при условии, что на данном компьютере установлен пакет Microsoft Office ХР или любое из перечисленных выше приложений). С по- мощью смарт-тегов в документе отмечаются отдельные слова или словосочета- ния, и пользователю предлагается выбрать одно из доступных для этого слова действий. Например, слово Moscow может быть распознано и отмечено как гео- графическое название, а словосочетание John Smith — как английское имя. Если слово или словосочетание отмечено как распознанное (в случае Word 2002 оно подчеркивается фиолетовыми точками, в случае Excel — выделяется фиолетовым треугольником в правом нижнем углу ячейки), пользователь может вывести на
368 Глава 8. Создание модулей расширения Microsoft Office экран список возможных действий с этим словом и выбрать одно из них. Напри- мер, пользователь может добавить в документ карту MapPoint 2002 с Москвой в центре, спланировать маршрут до Москвы, равно как и создать новый контакт Outlook для Джона Смита. Для этого достаточно поместить указатель мыши на распознанное слово или словосочетание, немного подождать, щелкнуть на кнопке Smart Tag Actions и выбрать одно из возможных действий в появившемся списке (рис. 8.6). Рис. 8.6. Пользовательский интерфейс смарт-тегов Иными словами, смарт-теги позволяют динамически распознавать пользова- тельский ввод и действовать в зависимости от результатов. Набор смарт-тегов, поставляемый с Microsoft Office, можно расширять своими смарт-тегами. Это означает, что мы можем определять собственные категории слов и словосочетаний, которые следует распознавать, и набор действий, которые можно с ними выполнять. Например, мы можем создать новую категорию смарт- тегов Borland products, описать строки с именами продуктов Borland и опреде- лить действия, которые можно сделать с этими именами (например, найти цену продукта в прайс-листе, содержащемся в рабочей книге Excel, открыть в web- браузере страницу, посвященную этому продукту, добавить в документ изобра- жение коробки с этим продуктом и т. д.). Следует заметить, что хотя смарт-теги и содержатся внутри документа, реа- лизация кода, выполняющего распознавание строк и выполнение действий над ними, находится за его пределами — она содержится в отдельной библиотеке, требующей регистрации на компьютере пользователя. Иными словами, в отли- чие от макросов VBA, при передаче документа с помощью электронной почты или при загрузке его из Интернета никакой код непосредственно в самом до- кументе не передается, что представляется благоприятным с точки зрения безо- пасности.
Создание смарт-тегов для Office ХР 369 Имеется несколько способов реализации смарт-тегов. Простейший из них заключается в создании XML-файла, описывающего категории словосочетаний, сами словосочетания и действия, которые с ними можно произвести. Пример по- добного файла приведен ниже: <FL:smarttagl1 st xmlns:FL= "urn:schemas-microsoft-com:smarttags:11st"> <FL:name>Borland Products</FL:name> <FL:lcid>1033</FL:lcid> <FL:description>This is a smart tag list with the names of Borland products</FL:description> <FL: morei nfourl>http://www.borland.com</FL:morei nfourl> <FL;smarttag type="MySmartTags#borprod"> <FL:capti on>Borland Products</FL:capti on> <FL:terms> <FL:termlist>VisiBrOKer. AppServer, AppCenter. DataSnap</FL:termli st> </FL:terms> <FL:actions> <FL:action id="BorProdActionl"> <FL:caption>Borland Web site</FL:caption> <FL:url>http://www.borland.com</FL:url > </FL:action> <FL:action id=" BorProdAction2"> <FL:caption>Borland Community Web site</FL:caption> <FL:url>http://communi ty.borland.com</FL:url > </FL:action> </FL:actions> </FL:smarttag> </FL:smarttaglist> Для того чтобы смарт-тег, определенный таким образом, стал доступен при- ложениям Office ХР, следует поместить этот файл в каталог C:\Program Files\ Common Files\Microsoft Shared\Smart Tag\Lists. Кроме того, следует убедиться, что в реестре имеются следующие ключи: HKEY_CURRENT_USER\Software\Mi crosoftXOffi ce\Common\ Smart Tag\Recognizers\{64AB6C69-B40E-40AF-9B7F-F5687B48E2B5} HKEY_C(JRRENT_USER \Software\Microsoft\Office\Common\ Smart Tag \Actions\{64AB6C69-B40E-40AF-9B7F-F5687B48E2B5} Здесь в фигурных скобках указано значение GUID для СОМ-сервера Micro- soft Office Smart Tag List, обслуживающего все смарт-теги, реализованные в виде XML-списков. Помимо этого, следует убедиться, что каждый из указанных двух ключей обладает строковым значением Li stDi rectory, идентифицирующим ката- лог, в котором хранятся эти XML-списки.
370 Глава 8. Создание модулей расширения Microsoft Office Подобные решения легко реализуются, но недостаточно расширяемы, а дей- ствия, определяемые в них, довольно примитивны (например, открыть заранее определенную web-страницу, если словосочетание распознано как принадлежа- щее к определенной категории). Если же мы хотим от смарт-тега более слож- ной функциональности, нам следует реализовать COM DLL. Это можно сделать с помощью любого средства разработки, поддерживающего создание COM DLL, в частности использовать для этой цели Delphi Professional или Enterprise любой версии, начиная с версии 3.0, что мы и сделаем. Требования к библиотекам, реализующим смарт-теги Каждый смарт-тег состоит из двух компонентов: распознавателя (recognizer), от- вечающего за распознавание словосочетания как принадлежащего к определен- ной категории и пометку его в документе, и обработчика (action), ответственного за вывод списка действий и выполнения одного из них в соответствии с выбором пользователя. Оба эти компонента должны быть COM-объектами, реализован- ными в виде библиотек, которые, в свою очередь, при необходимости использу- ются приложениями Microsoft Office ХР. Чтобы создать смарт-тег, нам следует сослаться на библиотеку типов Microsoft Smart Tags 1.0 type library (MSTAG.TLB), содержащую несколько COM-интерфейсов. В наших COM-объектах мы должны реализовать два из них: ISmartTagRecogmzer и ISmartTagAction. Описание свойств интерфейса ISmartTagRecogmzer приведено в табл. 8.1. Таблица 8.1. Свойства интерфейса ISmartTagRecognizer Свойство Описание Desc Описание распознавателя Name Имя распознавателя, отображаемое на вкладке Smart Tags диалогового окна AutoCorrect Options (открывается командой Tools ► AutoCorrect Options) в Word и Excel ProgID Программный идентификатор класса распознавателя SmartTagCount Число распознаваемых типов смарт-тегов Sma rtTagDownloadURL URL-адрес, переход по которому происходит при щелчке на кнопке More Smart Tags на вкладке Smart Tags диалогового окна AutoCorrect Options SmartTagName Уникальные идентификаторы типов смарт-тегов, поддерживаемых распознавателем Интерфейс ISmartTagRecogmzer экспонирует один-единственный метод — Recognize, который распознает символьные строки как смарт-теги. В табл. 8.2 приведено описание свойств интерфейса ISmartTagAction.
Создание смарт-тегов для Office ХР 371 Таблица 8.2. Свойства интерфейса ISmartTagAction Свойство Описание Desc Описание обработчика смарт-тега Name Наименование обработчика смарт-тега ProgID SmartTagCaption SmartTagCount Программный идентификатор класса обработчика смарт-тега Строка, отображаемая над списком кнопки Smart Tag Actions Число типов смарт-тегов, поддерживаемых соответствующим распознавателем SmartTagName VerbCaptlonFromID Уникальные идентификаторы типов обработчиков Наименования возможных действий в списке кнопки Smart Tag Actions VerbCount Число поддерживаемых обработчиков для данного типа смарт-тегов VerMD Уникальный идентификатор внутри смарт-тега, используемый приложением для поддержки взаимодействия с библиотеками, реализующими обработчики смарт-тегов других типов VerbNameFromID Название обработчика, используемое приложением Интерфейс ISmartTagAction также экспонирует один-единственный метод — InvokeVerb, который выполняется, когда пользователь выбирает соответствующий пункт в списке кнопки Smart Tag Actions. Зная свойства и методы интерфейсов ISmartTagRecogrizer и ISmartTagAction, мы можем приступить к их реализации. Создание распознавателей смарт-тегов В нашем примере мы создадим смарт-тег, который распознает названия продук- тов Borland и в зависимости от наименования распознанного продукта реализует тот или иной обработчик. Пусть каждый обработчик открывает web-страницу, посвященную выбранному продукту. В нашем примере мы можем поместить рас- познаватель и обработчик в одну и ту же библиотеку, но в общем случае это не обязательно. Запустим Delphi и создадим новую COM-библиотеку. Для этой цели выберем команду File ► New ► Other в главном меню среды разработки Delphi, перейдем на страницу ActiveX репозитария объектов и выберем значок ActiveX Library. После этого будет сгенерирован код «пустой» библиотеки: library Projectl; uses ComServ; exports 01IGetClassObject.
372 Глава 8. Создание модулей расширения Microsoft Office DllCanUnIoadNow. DllRegisterServer, DI 1Unregi sterServer; {$R *.RES} begin end. Переименуем наш проект в st.dpr. Теперь добавим к нашей библиотеке объект, реализующий интерфейс ISmartTagRecognizer, Для этого снова выберем команду File ► New ► Other, перей- дем на страницу ActiveX репозитария объектов и выберем значок COM Object, что приведет к запуску мастера создания COM-объектов (рис. 8.7). Рис. 8.7. Диалоговое окно COM Object Wizard В диалоговом окне COM Object Wizard нам нужно указать имя СОМ-класса (пусть он называется STR) и описание COM-объекта, значения в раскрывающихся списках Instancing и Threading Model можно оставить выбранными по умолча- нию. Заполняя поле Implemented Interface, вспомним, что нам нужно реализовать уже существующий интерфейс — приложение Word или Excel будет искать его имя в реестре и в самой библиотеке. Простейший способ сделать это — щелкнуть на кнопке List и выбрать интерфейс ISmartTagRecogmzer в списке доступных ин- терфейсов (рис. 8.8). Если этот интерфейс отсутствует в списке, нам следует убедиться, что па- кет Office ХР (или Word 2002, или Excel 2002) установлен на компьютере, ис- пользующемся для разработки смарт-тегов. Если это так, нужно щелкнуть на кнопке Add Library и найти файл MSTAG.TLB в каталоге C:\Program Files\Common Files\Microsoft Shared\Smart Tag.
Создание смарт-тегов для Office ХР 373 1 : nt etface Select io n Wizard ,>,4^ Д .... r... - • <• М1н*г—•- Interface Type Libiary Version Path GUID IS liderE vents COMCTL32.OCX 1.3 CAWIN NT\System32\ (373FF7F2E1 ISliderEvenls mscomctl ocx 2.0 CAWINN TKSystem32\ (FO8DF9538 ISmartStart ic whelp dll 1.0 CAProgram FilesKInlernet Explored Connection Wizard's (5D8D8F148 ISmartTagAction MSTAG TLB 1.0 CAProgram FilesKCommon FilesKMicrosoft SharedXSmart TagK (3B744D8FB ISmartTagProperties MSTAG TLB DM CAProgram FilesKCommon Files KM icr os oft SharedKSmart Tag\ (54F378< П ISmartT aqRecoqnizer MSTAG TLB 1 0 CAProgram FfesVCommon FifeAMictosoft SharedKSmart Taq'x КЯЙГИИЯЭ ISmartT agRecognizerSite MSTAG.TLB 1.0 CAProgram FrlesKCommon FrlesKMicrosott SharedXSmart TagK {9BF068D0B ISmlpAdmin smtpadm dll 1.0 C \WlNNT\Sy$tem32\inetsrv\ (1A04EA81 9_| ISmlpAdmirAlias smlpadm dll 1.0 CAWlNNT\System32\inetsrv\ (1A04EA85-9 ISmtpAdminDL smtpadm dll 1.0 C: \Wl NN T \Sy$tem32\inetsrv\ (1A04EA87-9 ISmlpAdminDomain smlpadm dll 1.0 C: KWIN N T \Sy$tem32\metsrv\ (1A04EA88-9 ; ISmlpAdmtnService smlpadm dll 1.0 C \WlNNTKSystem32\metsrv\ (1A04EA82-9 ISmtpAdminSessions ISmtpAdminUser 1 SmlpAdminVirlualDirecl... smtpadm. dH smlpadm.dll smlpadm. dH 1.0 1 0 1 0 C: \WIN N T KSy stem32\inet$rvK C AWIN N T \System32\rnetsrv\ C AWl N N T KSy stem32\inet$f v\ (1A04EA84-9 i (1A04EA86-9: f (1А04ЕА92-9Д1 J c ddLibiaiy | j *. J j Help | t' ...................Ч..........." .....П ...X. Л r. - ......................,4......„ о ».x. ............«..ОТ.. .......». .......... R............ .4 ....... ... jFffiished loacfing interfaces / Рис. 8.8. Выбор интерфейса ISmartTagRecognizer После щелчка на кнопке ОК в окне COM Object Wizard будут сгенерированы заготовки методов класса TSTR, реализующего интерфейс ISmartTagRecognizer. Те- перь можно создать их реализацию. Она может выглядеть, например, так: unit stl: fWAR/V SYMBOL_PLATFORM OFF} interface uses ComObj. ActiveX. st_TLB. StdVcl, SmartTagLibJLB; type TSTR = classCTAutoObject. ISmartTagRecognizer) protected function Get_Desc(LocaleID: SYSINT): WideString; safecall; function Get_Name(LocalelD: SYSINT): WideString; safecall; function Get_ProgId: WideString; safecall; function Get_SmartTagCount: SYSINT; safecall: function Get_SmartTagDownloadURL(SmartTagID: SYSINT): WideString; safecall; function Get_SmartTagName(SmartTagID: SYSINT): WideString; safecall; procedure Recognize(const Text: WideString:
374 Глава 8. Создание модулей расширения Microsoft Office Datatype: IF_TYPE: LocalelD: SYSINT; const Recogm'zerSite: ISmartTagRecognizerSite): safecal1; { Protected declarations } end; const BorProdArray: array[1..5] of String = ('Delphi', 'C++Builder', 'JBuilder'. 'Kylix'. 'JDataStore'); implementation uses ComServ; function TSTR.Get_Desc(LocaleID: SYSINT): WideString; begin result ;= 'The test smart tag'; end; function TSTR.Get_Name(LocaleID: SYSINT): WideString; begin result := 'Borland Web Site Smart Tag' end: function TSTR.Get_ProgId: WideString; begin result ;= 'st.STR': end; function TSTR.Get_SmartTagCount: SYSINT: begin result := 1; end; function TSTR.Get_SmartTagDownloadURL(SmartTagID; SYSINT): WideString: begin result := '': end: function TSTR.Get_SmartTagName(SmartTagID; SYSINT): Wi deStri ng; begin result := 'ComputerPressfborwebsite'; end; procedure TSTR.Recognize(const Text: WideString:
Создание смарт-тегов для Office ХР 375 DataType; IF_TYPE; LocalelD; SYSINT; const RecognizerSite: ISmartTagRecognizerSite); var PropertyBag: ISmartTagProperties: tx. txl. str: String; i, len. ix: Integer: begin for I := 1 to 5 do begin str := BorProdArray[i]; len := Length(str): ix := Pos(str, text): while ix > 0 do begin PropertyBag : = RecognizerSite.GetNewPropertyBag: tx := text; RecognizerSite.CommitSmartTag!'ComputerPress#borwebsite'. ix. len. PropertyBag); txl := Copy(tx, ix + len, Length(tx) - ix - len); tx ;= txl; ix := PosCstr, tx); end: end: end: initialization TAutoObjectFactory.Create!ComServer, TSTR. Class_STR. ciMultiInstance. tmApartment); end. Поясним, что содержится в приведенном выше коде. Первые три метода со- общают офисному приложению, что смарт-тег называется Borland Web Site Smart Tag, его описание представляет собой строку The test smart tag, а его программ- ный идентификатор равен st.STR. Метод Get_SmartTagCount возвращает едини- цу, означающую, что в данном примере мы реализуем один смарт-тег. Метод Get_SmartTagDownloadllRL возвращает пустую строку, означающую, что в этом при- мере мы не планируем производить обновления смарт-тегов через Интернет. Метод Get_SmartTagName возвращает уникальную строку (в нашем примере — ComputerPress#borwebsite) для идентификации нашего смарт-тега. Метод Recognize реализует алгоритм сканирования текста с целью нахожде- ния в нем строк, определенных в строковом массиве BorProdArray (этот массив содержит названия продуктов Borland). Если мы находим строку из этого массива, создается новый экземпляр интерфейса ISmartTagProperties (он описан в той же библиотеке MSTAG.TLB, ссылка на которую уже есть в нашем проекте). Этот эк- земпляр нужен для хранения свойств смарт-тега для найденного словосочетания. Далее нам следует сохранить сам смарт-тег в документе. После распознавания словосочетания его следует обработать. Поэтому сле- дующим шагом будет создание обработчика смарт-тегов.
376 Глава 8. Создание модулей расширения Microsoft Office Создание обработчика смарт-тега Для создания обработчика смарт-тега добавим к нашей библиотеке СОМ-объект, реализующий интерфейс ISmartTagAction. Чтобы это сделать, нам следует выбрать значок COM Object на странице ActiveX репозитария объектов, в диалоговом окне COM Object Wizard указать имя COM-класса (назовем его STA), оставить значения по умолчанию в раскрывающихся списках Instancing и Threading Model и ввести описание этого СОМ-объекта. Заполняя поле Implemented Interface, мы должны щелкнуть на кнопке List и выбрать интерфейс ISmartTagAction в списке доступ- ных интерфейсов. После этого мы получим заготовки для методов класса TSTA, реализующего интерфейс ISmartTagAction. Реализация этих методов может вы- глядеть так: unit st2: {$WARN SYMBOL_PLATFORM OFF} interface uses ComObj. ActiveX. st_TLB. SysUtils. StdVcl. SmartTagLib_TLB: type • TSTA = class(TAutoObject. ISmartTagAction) protected function Get_Desc(LocaleID: SYSINT): Wi deSt ring: safecal 1; function Get_Name(LocaleID: SYSINT): WideString: safecall: function Get_ProgId: WideString: safecall: functi on Get_Sma rtTagCapti on(Sma rtTagID. LocalelD: SYSINT): WideString; safecall: function Get_SmartTagCount: SYSINT: safecall; function Get_SmartTagName(SmartTagID: SYSINT): Wi deStri ng; safecal1: function Get_VerbCaptionFromID(VerbID: SYSINT; const ApplicationName: WideString; LocalelD: SYSINT): WideString; safecall; function Get_VerbCount(const SmartTagName; WideString): SYSINT: safecall: function Get_VerbID(const SmartTagName: WideString: VerMndex; SYSINT): SYSINT: safecall: function Get_VerbNameFromID(VerbID: SYSINT): WideString: safecall: procedure InvOKeVerbCVerMD: SYSINT; const ApplicationName: WideString:
Создание смарт-тегов для Office ХР 377 const Target: IDispatch: const Properties: ISmartTagProperties: const Text. Xml: WideString); safecall; end: implementation uses ComServ: function TSTA.Get_Desc(LocaleID: SYSINT): WideString; begin result := 'The test smart tag'; end; function TSTA.Get_Name(LocaleID; SYSINT): WideString; begin result := 'Borland Web Site Smart Tag'; end; function TSTA.Get_ProgId: WideString; begin result := 'st.STA': end: functi on TSTA.Get_Sma rtTagCapti on(SmartTagID. LocalelD: SYSINT): WideString; begi n if SmartTagId=l then result := 'Borland Product'; end; function TSTA.Get_SmartTagCount: SYSINT; begin result := 1; end; function TSTA.Get_SmartTagName(SmartTagID: SYSINT): WideString; begi n if SmartTagld = 1 then result := 'ComputerPress#borwebsite': end; function TSTA.Get_VerbCaptionFromID(VerbID: SYSINT: const ApplicationName: WideString; LocalelD: SYSINT): WideString:
378 Глава 8. Создание модулей расширения Microsoft Office begin if VerMD = 1 then result := 'Get product information from Borland web site’: end; function TSTA.Get_VerbCount( const SmartTagName: WideString): SYSINT: begin result := 1; end: function TSTA.Get_VerbID(const SmartTagName: WideString: Verbindex: SYSINT): SYSINT: begin result := 1: end; function TSTA.Get_VerbNameFromID(VerbID: SYSINT): WideString: begin if VerMD = 1 then result := 'BorProdVerbName' end: procedure TSTA.InvOKeVerbCVerMD: SYSINT: const ApplicationName: WideString: const Target: IDispatch: const Properties: ISmartTagProperties: const Text. Xml: WideString): var IE: Variant: ProdDir: String: begin ProdDir := Text: if Text = 'C++Builder' then ProdDir := 'bcppbuilder'; IE := CreateOleObject('InternetExplorer.Application'): IE.Navigate2('www.borland.com/'+ Ansi LowerCase(ProdDi r)); IE.Visible := True; end: initialization TAutoObjectFactory.Create(ComServer. TSTA. Class_STA, ciMultiInstance. tmApartment); end. Как и в предыдущем случае, реализация первых трех методов отражает тот факт, что смарт-тег называется Borland Web Site Smart Tag, его описание — The test smart tag, а его программный идентификатор — st.STA. Метод Get_SmartTagCaption возвращает строку, которая появится в верхней части раскрывающегося списка смарт-тега. Пусть это будет строка Borland Product.
Создание смарт-тегов для Office ХР 379 Метод Get_SmartTagCount возвращает 1, а метод Get_SmartTagName — уникальную строку ComputerPress#borwebsite для идентификации смарт-тега. Метод Get_VerbCount должен возвращать количество возможных действий, доступных для данного смарт-тега. В нашем примере мы реализуем одно дейст- вие, поэтому этот метод возвращает 1. Метод Get_VerbCaptionFromID должен воз- вратить название действия, которое появится в раскрывающемся списке смарт- тега. Пусть это будет действие Get product information from Borland web site. Метод Get_VerMD должен вернуть уникальный идентификатор действия внутри смарт- тега — пусть это будет 1. Метод Get_VerbNameFromID возвращает имя действия, используемое внутри приложения Office, — пусть это будет имя BorProdVerbName. Реализация действия должна быть осуществлена в методе InvokeVerb. Здесь мы напишем код, который будет выполнен, если пользователь выберет данное действие. В отличие от обсуждавшихся выше простых смарт-тегов, основанных на XML-списках, в реализации метода InvokeVerb может содержаться алгоритм любой степени сложности. В частности, в нем мы можем выводить на экран диа- логовые окна, запускать другие приложения, осуществлять поиск в базах данных или в реестре и т. д. Можно выполнять разные действия для Word и Excel — чтобы определить, какое из этих приложений инициировало вызов метода InvokeVerb, мы можем использовать параметр Appl 1 cati onName этого метода. В нашем же при- мере мы просто запустим Internet Explorer и откроем страницу с информацией о продукте, имя которого было распознано (адрес страницы зависит от распо- знанного названия продукта). Теперь нашу библиотеку можно скомпилировать и зарегистрировать (напри- мер, выбрав в меню Delphi команду Run ► Register ActiveX Server). Однако для ус- пешного применения созданного смарт-тега этого недостаточно. Ниже мы обсу- дим, что именно нужно для успешной поставки и тестирования таких библиотек. Поставка и тестирование библиотек, реализующих смарт-теги Для регистрации созданной библиотеки COM DLL как динамически загружае- мой и реализующей смарт-тег, нам следует найти (а возможно, и создать) сле- дующие разделы реестра: HKEY_CDRRENT_USER\Software\Microsoft\Office\Common\ Smart Tag\Recognizers\ HKEY_CURRENT_USER\Software\Microsoft\Office\Common\ Smart TagYAction Первый из этих разделов должен содержать подраздел, название которого равно идентификатору (ProgID или CLSID) COM-класса, реализующего интер- фейс ISmartTagRecognizer, а второй — COM-класса, реализующего интерфейс ISmartTagAction. Следует обратить внимание на то, что для тестирования смарт- тега достаточно идентификатора ProgID, но в большинстве случаев лучше ис- пользовать CLSID. Дело в том, что смарт-теги можно отключать из приложений Office, но это состояние для библиотек, зарегистрированных с использованием ProgID, сохраняется только в течение пользовательского сеанса. Результатом
380 Глава 8. Создание модулей расширения Microsoft Office может быть, например, такое поведение смарт-тега: пользователь отключил рас- познаватель в Word или Excel, заново запустил приложение, но распознаватель снова оказался активным. Заметим, что, в отличие от пакета Visual Basic, который обычно рекомендуется как основное средство разработки смарт-тегов, Delphi по- зволяет нам получить доступ к CLSID COM-объектов на этапе разработки — эти идентификаторы можно найти в редакторе библиотек типов (рис. 8.9) или в тек- сте соответствующего модуля (в нашем случае — в тексте модуля st_TLB.pas). Рис. 8.9. Идентификатор CLSID созданного COM-объекта в редакторе библиотеки типов Для регистрации смарт-тега можно создать соответствующий REG-файл и по- ставлять его вместе с библиотекой, реализующей смарт-тег, либо позаботиться о создании ключей реестра в приложении InstallShield Express, входящем в ком- плект поставки Delphi, которое позволяет это делать. Можно также переписать процедуру регистрации библиотеки, реализующей смарт-тег, добавив в нее опе- раторы создания нужных разделов и ключей: procedure TSTRFactory.UpdateRegistry(Register: Boolean); var Reg: TRegistry: Sectionl.Section2: String: begin Reg := nil; Sectionl := '\Software\Microsoft\Office\Common\'+ 'Smart Tag\Recognizers\'+GuidToString(CLASS_STR); Section2 := '\Software\Microsoft\Office\Common\+ 'Smart Tag\Actions\'+GuidToString(CLASS_STA);
Создание смарт-тегов для Office ХР 381 try Reg := TRegistry.Create; if Register then begin inherited UpdateRegistry(Register); Reg.OpenKeyfSection1. True): Reg.CloseKey; Reg.0penKey(Section2. True); Reg.CloseKey: end else begin Reg.DeleteKey(Sectionl); Reg. De 1 eteKey (Sect i on2); inherited UpdateRegistry(Register); end; finally Reg.Free; end; end; initialization TSTRFactory.Create(ComServer. TSTR, Class_STR. ciMultilnstance. tmApartment); end. Как протестировать смарт-тег? Во-первых, нам нужно удостовериться, что уровень безопасности при исполь- зовании модулей расширения Office средний или низкий и что приложения Office допускают применение всех установленных модулей расширения; для этого следует вызвать диалоговое окно Security, выбрав в меню Word или Excel команду Tools ► Macro ► Security (рис. 8.10). Рис. 8.10. Диалоговое окно Security Microsoft Word 2002
382 Глава 8. Создание модулей расширения Microsoft Office Во-вторых, нам следует закрыть все приложения Microsoft Office, а также Internet Explorer. Кроме того, рекомендуется заглянуть в окно Processes прило- жения Task Manager и выгрузить процессы OUTLOOK.EXE, WINWORD.EXE, EXCEL.EXE и IEXPLORE.EXE. В-третьих, нам следует запустить Microsoft Word 2002 или Microsoft Excel 2002 и проверить, найден ли наш смарт-тег (в данном случае Borland Web Site Smart Tag). Если да, мы увидим его название на вкладке Smart Tags диалогового окна AutoCorrect (рис. 8.11). Его можно вывести на экран, выбрав команду Tools ► Autocorrect Options. Рис. 8.11. Диалоговое окно AutoCorrect Если мы не можем найти наш смарт-тег в диалоговом окне AutoCorrect, следует проверить, не отключен ли он из-за сбоя при загрузке библиотеки приложением Office. Для этой цели следует заглянуть в раздел реестра, соответствующего распознавателю: HKEY_CURRENT_USER\Software\Mi crosoftWf f 1 ce\Common \Smart Tag\Recognizers\st.STR Если мы найдем в этом разделе ключ Status, это означает, что смарт-тег от- ключен либо вручную пользователем из приложения Office, либо самим прило- жением из-за сбоя при загрузке DLL. Если первое исключается, стоит заняться отладкой созданной библиотеки.
Создание смарт-тегов для Office ХР 383 Если смарт-тег обнаружен, можно протестировать его работу. Мы видим, что при появлении в документе названий продуктов Borland они помечаются как смарт-теги, и при выборе действия Get product information from Borland web site (рис. 8.12) запускается Internet Explorer и открывается страница, посвященная выбранному продукту. £3 Microsoft Excel - Pricetist Fite gd»t View s Insert Format Tod* QaU J^ndow tjcfc - ® X : I У X tto e • - • < x - .tt fl $ V 7 ’ ” B8 + • HDB1360WWCS180 А Б " c . IT: 1 Borland Products Price List ... i 2 - 3Name SKU Price j 4 Delphi j I' 5 iDelphi 6 Enterprise Full System HDE1360WWFS180 $ 3 040,00 6 :Delphi 6 Enterprise Upgrade HDE1360WWCS180 $ 2 430,00 7 ‘Delphi 6 Professional Full System HDB1360WWFS180 $ 1 015,00 —* 8 ^Delphi 6 Professional Upgrade нов i36owwcsi8o $ 405,00 9 iC++Builder 10 :C++ Builder5 Standard Borland Product !. 5 L 11 C++Builder 5 Professional Full System 12 C++Builder 5 Professional Upgrade i I 13 C++ BuilderS Enterprise Full System Remove this Smart Tag 14 :C++Builder5 Enterprise Upgrade ~ 1 I IS -C++ Builder 5 Manual Set 16 JBuilder j IQ JBuilder 5 Enterprise Full System JBE0050WWFS180 $3 040,00 18‘JBuilder 5 Professional Full System JBB0050^WFS 180 $ 1 015,00 19 iKylix 20 ‘Kylix Desktop Development Edition New User HDC701OWWFS1 80 $205,00 V; к « ► n!\sheetl /Shee~t2 Z Sheets / |«| 1 »|П“ z ' ... ' - ' > > i ► • Security... j v Ready : : . .. Рис. 8.12. Тестирование созданного смарт-тега в Excel 2002 Мы также можем убедиться, что данный смарт-тег корректно взаимодействует с другими смарт-тегами. Например, поскольку средство разработки Delphi названо в честь известного греческого города, это слово распознается не только как про- дукт Borland, но и как город в Европе, и, следовательно, распознается и нашим смарт-тегом, и смарт-тегом, установленным вместе с Microsoft MapPoint 2002 (рис. 8.13). Таким образом, мы научились создавать и регистрировать смарт-теги для Office ХР. Мы убедились, что эта задача не очень сложна — нужно всего лишь создать COM-библиотеку, реализовать в ней два интерфейса и обеспечить ее корректную регистрацию.
384 Глава 8. Создание модулей расширения Microsoft Office Рис. 8.13. Взаимодействие созданного смарт-тега с другими смарт-тегами Заключение В настоящей главе мы обсудили вопросы создания модулей расширения для при- ложений Microsoft Office. Мы узнали, что: Ж начиная с Office 2000, появилась возможность написания модулей расшире- ния COM (COM Add-Ins), которые интегрируются с любым из приложений Microsoft Office; Ж модуль расширения представляет собой СОМ-сервер, который специаль- ным образом зарегистрирован в операционной системе и реализует интерфейс IDTExtensi bi 1 ity2; В модулю расширения доступен интерфейс IDispatch вызывающего приложения, поэтому модуль расширения может модифицировать интерфейс этого прило- жения и вызывать его методы. Мы рассмотрели вопросы обработки событий СОМ, регистрации и отладки модулей расширения, реализации функциональности модуля расширения и созда- ния модулей расширения, работающих с несколькими приложениями Microsoft Office. Мы также обсудили вопросы создания смарт-тегов для Microsoft Office ХР. Мы узнали, что: И смарт-теги обеспечивают динамическое распознавание пользовательского ввода и выполнение действий в зависимости от результатов этого распознавания; В набор смарт-тегов, поставляемый с Microsoft Office, можно расширять собст- венными смарт-тегами;
Заключение 385 В каждый смарт-тег состоит из двух компонентов: распознавателя (recognizer), отвечающего за распознавание словосочетания как принадлежащего к опре- деленной категории и пометку его в документе, и обработчика (action), ответ- ственного за вывод списка действий и выполнения одного из них в соответст- вии с выбором пользователя; Ж чтобы создать смарт-тег, реализующий произвольную функциональность, нам следует создать библиотеки, содержащие COM-объекты, реализующие ин- терфейсы ISmartTagRecognizer и ISmartTagAction. Мы обсудили проблемы поставки и тестирования библиотек, реализующих смарт-теги. Итак, теперь мы знаем, каким образом создавать модули расширения и смарт- теги для приложений Microsoft Office. Напомним, что подобные технологии часто используются в компаниях, в которых приложения Microsoft Office являются основным инструментом работы пользователей, и позволяют создавать недоро- гие решения, способные повысить эффективность работы пользователей и обес- печить их дополнительными сервисами. Отметим, однако, что в качестве COM-серверов и COM-клиентов можно при- менять не только приложения Microsoft Office, но и сами 32-разрядные опера- ционные системы семейства Windows. Примеры подобных приложений будут рассмотрены в следующих двух главах.
ГЛАВА 9 Применение СОМ-объектов из состава Windows Эта глава посвящена использованию СОМ-объектов, создаваемых различными приложениями. Поскольку число таких приложений велико и с каждым годом становится все больше, будут рассматриваться только те COM-объекты, которые стали частью операционной системы Windows. Очевидно, что все они создаются приложениями, распространяемыми компанией Microsoft. Создание ярлыков Как известно, ссылки для запуска приложений сохраняются в соответствующих каталогах в виде ярлыков (shortcuts) — файлов с расширением *.lnk. Это двоичные файлы, имеющие достаточно сложную структуру. Для их создания в Windows 3.x использовался динамический обмен данными (Dynamic Data Exchange, DDE) с приложением Program Manager, постоянно запущенным при работе под управ- лением Windows 3.1/3.11. Для создания такого файла в Windows 95/98/NT/ 2000/ХР можно также использовать динамический обмен данными с Program Manager, но это приложение нужно обязательно предварительно запустить (на- пример, выбрав в главном меню команду Start ► Run и введя команду progman в поле открывшегося диалогового окна); в противном случае команды DDE не станут выполняться. Кроме того, компанией Microsoft было объявлено о недопустимости использования DDE при создании новых приложений; поддержка динамического обмена данными сохраняется только с целью обеспечения работоспособности ранее созданных приложений. Конечно, можно создать ярлык непосредственно путем записи двоичных дан- ных, однако для этой цели необходимо знать формат такого файла. Вместо этого можно воспользоваться тем, что программный интерфейс (API) оболочки (shell) Microsoft Windows полностью базируется на СОМ-технологии. Рассмотрим простейший пример, использующий этот факт. Поместим кнопку на главную форму приложения и напишем для нее обработчик события OnClick, создающий на рабочем столе Windows ярлык этого приложения: implementation uses ComObj, ActiveX. ShiObj: procedure TForml.ButtonlClick(Sender: TObject):
Создание ярлыков 387 var LinkFile: IPersistFI1e; Shell Object: IUnknown; Shell Link: IShellLink; FileName. Shortcutposition; String; WShortcutPosition; WideString; P: PItemIDList: C: array[0..1000] of char; begin Shellobject ;= CreateComObject(CLSID_ShellLink); LinkFile := Shell Object as IPersistFi1e: ShellLink := ShellObject as IShellLink; FileName := ParamStr(O); Shel1 Li nk.SetPath(Pchar(Fi1 eName)) ; Shel1 Li nk.SetWorki ngDi rectory!Pchar(ExtractFi 1 ePath( FileName))): if SHGetSpecialFolderLocation(Handle. CSIDL_DESKTOP. P)= NOERROR then begin SHGetPathFromIDList(P, C): Shortcutposition := StrPas(C): Shortcutposition := Shortcutposition + '\TheProgram.lnk': WShortcutPosition := Shortcutposition; LinkFile.Save(PWChar(WShortcutPosition). False): end; end; При выполнении этого кода сначала создается COM-объект, который под- держивает несколько стандартных СОМ-иптерфейсов, в частности IShellLink и IPersistFi 1 е. В интерфейсе IShellLink следует определить ряд обязательных па- раметров: имя исполняемого файла, путь к нему, рабочий каталог приложения. Можно также определить некоторые необязательные параметры, например, вы- брать другой значок для ярлыка или указать, в каком состоянии должно быть при запуске главное окно приложения — свернутом, развернутом или восстанов- ленном. Интерфейс IPersistFi 1е служит для сохранения или считывания файла с рас- ширением *.lnk. В качестве параметра он использует один из стандартных ката- логов Windows — Desktop, Program Files и др. Обратите внимание на то, что никогда не следует указывать имена этих ката- логов в явном виде — в различных языковых версиях Windows они могут быть разными. Например, каталогу Main Menu/Program Files английской версии Win- dows соответствует каталог Главное меню/Программы соответствующей русской версии Windows. Следует также помнить, что имена, присваиваемые по умолчанию при установке Windows всем специальным каталогам, могут быть впоследствии изменены. По этой причине для получения физического адреса специальных ката- логов необходимо использовать метод SHGetSpecialFolderLocation и вслед за ним — метод SHGetPathFromIDList. Эти методы вместе со списком констант, определяющих
388 Глава 9. Применение СОМ-объектов из состава Windows тип специальных каталогов, приведены в модуле ShiObj. Две другие полезные константы: » CSIDL_PROGRAMS — возвращает ссылку на каталог, содержащий программы для текущего пользователя; » CSIDL_COMMON_PROGRAMS — возвращает ссылку на каталог, содержащий программы, общие для пользователей данного компьютера. Получение уведомлений от Windows Explorer Путем получения уведомлений можно анализировать сообщения о манипуляциях с каталогами в Windows Explorer и обрабатывать их. Это даст пользователю возможность разрешать или запрещать копирование (перемещение, удаление, изменение имени) каталога. В проектах такого типа применяется интерфейс ICopyHook, определенный в модуле ShlObj. Создание проекта должно начинаться с выбора команды File ► New ► Other и активизации значка ActiveX Library на странице ActiveX репозитария объектов. Далее требуется выбрать команду File ► New Unit и создать новый модуль. Текст модуля необходимо вводить вручную: unit UHook: interface uses ShlObj. ComObj. Windows: const CLSID_WEHook: TGUID = '{9C123760-65F8-11D2-AEF9-444553540000}': type TWEHook = cl ass(TComObject, ICopyHook) public function CopyCallbackCWnd: HWND: wFunc. wFlags: UINT: pszSrcFile: PAnsiChar: dwSrcAttribs: DWORD: pszDestFile: PAnsiChar: dwDestAttribs: DWORD): UINT: stdcall: end: implementation uses Dialogs. Forms. ShellAPI, SysUtils. ComServ: type TWEHookFactory = class(TComObjectFactory)
Получение уведомлений от Windows Explorer 389 public procedure UpdateRegistry(Register: Boolean): override: end: function TWEHook.CopyCallback(Wnd: HWND; wFunc. wFlags: DINT: pszSrcFile: PAnsiChar: dwSrcAttribs: DWORD: pszDestFile: PAnsiChar: dwDestAttribs: DWORD): UINT: var Msg: String; begin Application.Handle := Wnd: Msg := ": case WFunc of FO_COPY: Msg := Format('Do You really want to copy' + ' the directory Xs into Xs?'. [pszSrcFile. pszDestFile]): FO_DELETE: Msg := Format('Do You really want to copy'+ ' the diredetory Xs?'. [pszSrcFile]): FO_MOVE: Msg := Formatf'Do You really want to move '+ ' the directory Xs into Xs?', [pszSrcFile. pszDestFile]): FO_RENAME: Msg := Formate'Do You really want to rename '+ ' the directory Xs to '+ 'new name Xs?'. [pszSrcFile, pszDestFile]): end; if Length(Msg) > 0 then Result := Messaged g( Msg. mtConfirmation. [mbYes. mbNo. mbCancel], 0) else Result := id_Yes; end: procedure TWEHookFactory.LlpdateRegistry(Register: Boolean): begin if Register then begin CreateRegKey('Directory\shellex\CopyHookHandlers\WEHook'. ". GUIDToString(CLSID_WEHook)): CreateRegKey(' CLSID\ '+GLIIDToString(CLSID_WEHook)+ '\InprocServer32'. 'Thread)ngModel', 'Apartment'): inherited UpdateRegistry(Register); end else begin DeleteRegKey('Directory\shellex\CopyHookHandlers\WEHook'); inherited UpdateRegistry(Register): end; end;
390 Глава 9. Применение СОМ-объектов из состава Windows initialization TWEHookFactory.Create(ComServer, TWEHook, CLSID_WEHook, 'WEHook', 'WE Folder Test Hook'. ciMultiInstance); end. В данном проекте необходимо реализовать интерфейс ICopyHook, в котором определен один метод — CopyCal Iback с соответствующим списком параметров. Для его реализации используется класс TComObject. Параметр WFunc содержит ин- формацию об изменениях, которые пользователь осуществляет с данным катало- гом, параметры pszSrcFI 1 е и pszDestFi 1 е — старое и новое имена данного каталога (содержащие полный путь). Возвращаемый результат содержит информацию о том, согласен ли пользователь с проведением данной операции. При регистрации сервера, перехватывающего сообщения от Windows Explorer, необходимо внести записи в секцию Di rectory/Shel 1 Ex реестра Windows. Внесение такой записи не предусмотрено стандартной процедурой регистрации фабрики классов TComObj ectFactory, поэтому необходимо переписать ее метод UpdateRegi stry. Вносимая запись представляет собой ссылку на CLSID интерфейса. Кроме того, в реестре необходимо указать, что данный сервер работает в многопоточном ре- жиме и соответствует требованиям модели разделенных потоков (см. главу 6). После компиляции проекта и регистрации сервера с помощью команды Run ► Register ActiveX server необходимо перезагрузить компьютер (в некоторых случаях сервер может начать работу и без перезагрузки). Для тестирования приложения необходимо в Windows Explorer создать ката- лог и попытаться изменить его название, скопировать его в другой каталог или удалить. В результате этих действий будут выводиться сообщения, подобные приведенному на рис. 9.1. Рис. 9.1. Уведомление Windows Explorer Получение подобных уведомлений от Windows Explorer можно использовать в приложениях, которые нуждаются в информации об изменении имен своих каталогов. Создание окон просмотра данных в Windows Explorer Windows Explorer позволяет просматривать не только файловую систему, но и дан- ные, которые не имеют непосредственного отношения к файловой системе. Хоро- ший пример — стандартные приложения Microsoft — Панель управления, Корзина,
Создание окон просмотра данных в Windows Explorer 391 Сетевое окружение, Принтеры. Доступ к этим приложениям осуществляется по- средством выбора соответствующего значка в Windows Explorer, при этом в правой нижней части окна Windows Explorer появляется содержимое соответствующих хранилищ, которые никакого отношения к реальной файловой системе не имеют. Для создания подобного приложения необходимо реализовать интерфейс IShell Folder, описывающий папку в Windows Explorer, и интерфейс IShel I View, который создает окно для просмотра данных в правом нижнем углу Windows Explorer. Оба эти интерфейса следует поместить во внутрипроцессный сервер автоматизации. Реализация интерфейса IShellFolder приведена ниже: unit IShlFold: interface uses Windows. ActiveX, CommCtrl. ShellAPI, RegStr. Messages. ComObj. ComServ. ShlObj. Classes. IShlView. Dialogs: const CLSID_CustomShellFolder: TGUID = ' {BFO294O1-68CC-11D2-9B02-0000E844A5C5}’: type TCustomShellFolder = class(TComObject. IShellFolder. IPerslstFolder) protected function IPerslstFolder.Initialize = Perslstlnltialize: public {IShellFolder} function ParseDisplayNameChwndOwner: HWND; pbcReserved: Pointer: IpszDisplayName: POLESTR: out pchEaten: ULONG; out ppidl: PItemIDList: var wAttributes: ULONG): HResult: stdcall; function EnumObjects(hwndOwner: HWND: grfFlags: DWORD: out EnumIDList: lEnumIDLIst): HResult: stdcall: function BindToObject(pidl: PItemIDList: pbcReserved: Pointer: const riid: TIID: out ppvOut): HResult: stdcall: function BindToStorage(pidl: PItemIDList: pbcReserved: Pointer; const riid: TIID: out ppvObj): HResult: stdcall: function ComparelDs(1 Pa ram: LPARAM; pi dll. pidl2: PItemIDList): HResult: stdcall: function CreateViewObjectChwndOwner: HWND: const riid: TIID: out ppvOut): HResult; stdcall:
392 Глава 9. Применение СОМ-объектов из состава Windows function GetAttrlbutesOf(cidl: UINT; var apidl: PltemlDList: var rgflnOut: UINT): HResult; stdcall; function GetUIObjectOf(hwndOwner: HWND; cidl: UINT: var apidl: PltemlDList; const riid: TIID; prgflnOut: Pointer; out ppvOut): HResult: stdcall; function GetDisplayNameOf(pidl: PltemlDList; uFlags: DWORD; var IpName: TStrRet): HResult; stdcall: function SetNameOfChwndOwner: HWND; pidl: PltemlDList; IpszName: POLEStr: uFlags: DWORD; var pidlOut: PltemlDList): HResult; stdcall: {IPersist} function GetClassID(out classID: TCLSID); HResult: stdcal1: function Persistlnitialize(pidl: PltemlDList): HResult: virtual: stdcall; end; implementation uses Registry; type TshellFolderObjectFactory = class(TComObjectFactory) public procedure UpdateRegistry(Register: Boolean); override: end; function TCustomShellFolder.ParseDisplayName(hwndOwner: HWND; pbcReserved: Pointer; 1pszDisplayName: POLESTR: out pchEaten: ULONG: out ppidl: PltemlDList; var dwAttributes: ULONG): HResult; begin Result := E_NOTIMPL; end: function TCustomShellFolder.EnumObjectsChwndOwner: HWND; grfFlags: DWORD: out EnumIDList: lEnumIDList): HResult; begin Result := E_NOTIMPL; end; function TCustomShellFolder.BindToObject(pidl: PltemlDList; pbcReserved: Pointer: const riid: TIID; out ppvOut); HResult;
Создание окон просмотра данных в Windows Explorer 393 begin Result := E_NOTIMPL: end; function TCustomShel1 Folder.BindToStorage( pidl: PItemIDList; pbcReserved; Pointer; const riid: TIID: out ppvObj); HResult; begin Result := E_NOTIMPL; end: function TCustomShellFolder.ComparelDsdParam: LPARAM; pidll,pidl2: PItemIDList): HResult: begin Result := E_NOTIMPL: end; function TCustomShellFolder.CreateV1ew0bject(hwnd0wner: HWND: const riid: TIID: out ppvOut): HResult; var SV:IShellView; begi n try Pointer(ppvOut) := nil; if IsEqualGUIDIriid. IShellView) then begin try SV := CreateComObject(CLSID_CustomShellView) as IShellView; except Colnitialize(nil): SV := CreateCom0bject(CLSID_CustomShellV1ew) as IShellView: end; if Assigned(SV) then begin SV._AddRef: Polnter(ppvOut):=Poi nter(SV): end: Result := S_OK: end else Result := E_NOINTERFACE: except on E: EOleSysError do Result := E.ErrorCode; else Result : = EJJNEXPECTED: end: end; function TCustomShelIFolder.GetAttrlbutesOfCcidl: DINT; var apidl: PItemIDList: var rgflnOut: DINT): HResult;
394 Глава 9. Применение СОМ-объектов из состава Windows begin Result := E_NOTIMPL: end: function TCustomShel1 Folder.GetUIObjectOf ( hwndOwner: HWND: cidl: UINT: var apidl: PItemIDList; const riid: TIID: prgflnOut: Pointer: out ppvOut): HResult: begin Result := E_NOTIMPL: end: function TCustomShel1 Folder.GetDisplayNameOf(pidl: PItemIDList: uFlags; DWORD: var IpName: TStrRet): HResult: begin Result := E_NOTIMPL: end; function TCustomShellFolder.SetNameOflhwndOwner: HWND: pidl: PItemIDList: IpszName: POLEStr; uFlags: DWORD: var ppidlOut: PItemIDList): HResult: begin Result : = E_NOTIMPL; end: { IPerslstFolder } function TCustomShel1 Folder.GetClassID( out classID: TCLSID): HResult: begin classID := CLSID_CustomShellFolder; Result := NOERROR: end: function TCustomShel1 Folder.Persi stlniti al ize( pidl: PItemIDList): HResult: begin Result := NOERROR: end: {} procedure TShel1 FolderObjectFactory.UpdateRegi stry( Register: Boolean); var Reg: TRegistry: L: DWORD: S: String: N: Integer;
Создание окон просмотра данных в Windows Explorer 395 begin if Register then begin inherited UpdateRegistry(Register); Reg : = ni 1: try Reg := TRegistry.Create: Reg.RootKey := HKEY_CLASSES_ROOT: Reg.OpenKey(’CLSID\'+ GUIDToString(CLSID_CustomShellFolder)* '\InprocServer32'. True); Reg.WriteString('Thread!ngModel'. 'Apartment'); Reg.CloseKey: Reg.OpenKey('CISID\'+ GUIDToString(CLSID_CustomShellFolder)* '\ShellFolder', True); Reg.WriteString(", "); L := SFGAO_FOLDER; Reg.WriteBinaryData('Attributes', L. SizeOf(L)); Reg.CloseKey; Reg.OpenKey('CLSIDX' + GUIDToString(CLSID_CustomShellFolder)* 'XDefaultlcon'. True); SetLength(S. MAX_PATH); N := GetModuleFileName(hInstance. @S[1], MAX_PATH): SetLength(S. N): Reg.WriteString(''. S); Reg.RootKey : = HKEY_LOCAL_MACHINE; Reg.OpenKey('SoftwareXMi crusoftXWi ndowsXCurrentVersi on' + '\Explorer\Desktop\Namespace\' + GUIDToString(CLSID_CustomShellFolder). True); Reg.WriteString(''. 'Shell Test view object'): Reg.CloseKey: finally Reg.Free; end: end else begin Reg := nil; try Reg := TRegistry.Create; Reg.RootKey : = HKEY_LOCAL_MACHINE; Reg.DeleteKey( 'SoftwareXMi crosoftXWi ndowsXCurrentVersi on\' + 'ExplorerXDesktopXNamespaceX'+ GUIDToStri ng(CLSID_CustomShel 1 Folder)); Reg.CloseKey: finally if Assigned(Reg) then Reg.Free; end:
396 Глава 9. Применение СОМ-объектов из состава Windows inherited UpdateRegistry(Register): end: end; initialization TShel1 FolderObjectFactory.Create(ComServer. TCustomShel 1 Folder. CLSID_CustomShellFolder. ". 'Inprise fish viewer'. ciSinglelnstance); end. Интерфейс IShellFolder содержит реализацию одного метода — CreateView. Этот метод будет вызываться всякий раз при выборе в Windows Explorer папки, описан- ной в IShellFolder. Имя папки будет соответствовать текстовому описанию фаб- рики классов TShel 10bjectFactory — в данном случае «Inprise fish viewer». В этом методе необходимо создать интерфейс IShellView и передать указатель на него. Следует обратить внимание на защищенный блок try...except...end. Если при создании рабочей копии интерфейса возникает исключение, это значит, что запрос к интерфейсу IShellView был выполнен не из главного потока Windows Explorer (Explorer, как известно, работает в многопоточном режиме). Соответст- венно, в нем не был произведен вызов метода Coinitialize, поэтому СОМ-объект не может быть создан. В этом случае необходимо вызвать метод Coinitialize пе- ред созданием СОМ-объекта. Регистрация фабрики классов IShell Fol den в системном реестре существенно отличается от регистрации стандартной фабрики классов сервера автоматиза- ции. Прежде всего, она должна поддерживать модель разделенных потоков при работе в многопоточном режиме. В Delphi версий 4 и выше это достигается добав- лением еще одного параметра в конструктор фабрики классов, а в Delphi 3 надо отдельно описать этот параметр под именем ThreadingModel в секции CLSID\<IID>\ InprocServer32. Здесь <1 ID> — идентификатор созданного интерфейса IShell Fol den: BF029401-68CC-11D2-9B02-0000E844A5C5 Кроме того, необходимо создать вложенную секцию с именем Shell Fol den в сек- ции CLSID\<IID> и поместить туда двоичные данные, содержащие флаги атрибу- тов вновь созданного объекта просмотра данных. Имя этих флагов — Attributes. Они определены в модуле ShlObj.pas и их имена начинаются с префикса SFGAO_. В данном примере установим эти флаги равными константе SFGAO FOLDER, что озна- чает просто отображение папки. Другие полезные флаги перечислены ниже. SFGAO_HASSUBFOLDER = $80000000; Содержит внутри другие элементы, Windows Explorer ставит значок в виде символа плюс (+), позволяющий открыть данную папку. SFGAO_DROPTARGET = $00000100: Поддерживает технологию перетаскивания (drag-and-drop), в таком прило- жении должен быть реализован интерфейс IDropTarget (см. ниже). SFGAO_HASPROPSHEET = $00000040;
Создание окон просмотра данных в Windows Explorer 397 Поддерживает страницу свойств. При щелчке правой кнопки мыши на объекте появляется меню, содержащее команду Properties, при выборе которой открыва- ется окно свойств данного объекта. Как сделать собственную страницу свойств, мы объясним в разделе «Добавление вкладок в диалоговое окно свойств файла». Без установки значения ключа Attributes папка не будет отображаться в Windows Explorer. В секции CLSID\<IID>\ полезно создать вложенную секцию Defaulticon и поместить туда ссылку на значок, который появится в Windows Explorer. При отсутствии этой вложенной секции будет показана стандартная папка. Помимо этого, необходимо создать вложенную секцию <IID> в разделе HKEY_LOCAL_MACHINE системного реестра в секции: Software\Microsoft\Windows\CurrentVersion\Explorer\ Desktop\Namespace\ В данном случае <IID> соответствует следующему значению: BF029401 - 68СС -11D2-9B02-0000Е844А5С5 В эту секцию можно поместить краткое текстовое описание просматривае- мых данных. Все необходимые записи в системный реестр (и их удаление) вы- полняет переписанный в данном примере метод UpdateRegistry фабрики классов TComObjectFactory. Реализация интерфейса IShellView приведена ниже: unit IShlView: 1nterface uses Windows. ActiveX. Shell API, ComObj, ComServ. ShlObj. CommCtrl, UTestF; const CLSID_CustomShellView: TGUID= '{BF029403-68CC-11D2-9B02-0000E844A5C5}': type TCustomShellView = classCTComObject. IShelIView) private FFolderSetti ngs: TFolderSetti ngs; FShellBrowser: IShellBrowser; FHWndParent: HWND; FForm: TForml: public {101 eWindow Methods} function GetWindow(out wnd: HWnd): HResult: stdcall: function ContextSensiti veHelp(fEnterMode:BOOL): HResult: stdcall: {IShe11 View Methods} function TranslateAccelerator(var Msg: TMsg): HResult: stdcall:
398 Глава 9. Применение СОМ-объектов из состава Windows function EnableModel ess(Enable: Boolean): HResult; stdcall; function UIActivate(State: UINT): HResult: stdcall: function Refnesh: HResult: stdcall: function CreateViewWindow(PrevView: IShellView: var FolderSettings: TFolderSettings: Shell Browser: IShellBrowser: var Rect: TRect; out Wnd: HWND): HResult: stdcall; function DestroyViewWindow: HResult: stdcall: function GetCurrentInfo( out FolderSettings: TFolderSettings): HResult; stdcall; function AddPropertySheetPages(Reseved: DWORD: var IpfnAddPage: TFNAddPropSheetPage; IParam: LPARAM): HResult; stdcall; function SaveViewState: HResult; stdcall; function Selectltem(pidl: PItemIDList; flags:UINT): HResult: stdcall; function GetItemObject(Item: UINT; const iid: TIID; var IPtr: Pointer): HResult; stdcall; end; implementation uses Dialogs, SysUtils. Forms; type TshellViewFactory = class(TComObjectFactory) public procedure UpdateRegistry(Reg1ster: Boolean): override; end: function TCustomShellView.GetWindow(out wnd: HWnd): HResult; begin if Assigned(FForm) then Wnd := FForm.Handle el se Wnd := 0: Result := NOERROR; end; function TCustomShel1 Vi ew.ContextSensiti veHelp(fEnterMode: BOOL): HResult; begin Result := E_NOTIMPL; end;
Создание окон просмотра данных в Windows Explorer 399 function TCustomShel1 View.TranslateAccelerator(var Msg: TMsg): HResult; begin Result : = E_NOTIMPL; end; function TCustomShel1 View.EnableModel ess(Enable: Boolean): HResult: begin Result := E_NOTIMPL; end; function TCustomShellView.UIActivate(State: UINT): HResult: var S: String; begin case TSVUIAEnums(State) of SVUIA_DEACTIVATE: S := 'Deactivate view’; SVUIA_ACTIVATE_NOFOCUS: S := 'Activate view without focus': SVUIA_ACTIVATE_FOCUS: S := 'Activate view with focus': SVUIAJNPLACEACTIVATE: S := 'Activate view for inplace-activation within ActiveX control': end: FShel1 Browser.SetStatusTextSB(Stri ngToOl eStr( 'IShellView.UIActivate: ' + S)); Result := NOERROR; end; function TCustomShel1 View.Refresh: HResult: begin FShel1 Browser.SetStatusTextSB(Stri ngToOl eStr( 'IShellView.Refresh')); Result : = E_NOTIMPL: end; function TCustomShel1 View.CreateViewWindow(PrevView: IShellView; var FolderSettings: TFolderSettings: Shell Browser: IShellBrowser; var Rect: TRect: out Wnd: HWND): HResult; begin FfolderSettings := FolderSettings; Fshell Browser := Shell Browser; if Assigned(FShel1 Browser) then FShel 1 Browser.GetWi ndow(FHWndParent)
400 Глава 9. Применение СОМ-объектов из состава Windows else FHWndParent := 0; if not Assigned(FForm) then FForm := TForml.Create(nil); Wnd := FForm.Handle; if FHWndParent <> 0 then begin SetParent(Wnd. FHWndParent): with FForm do begin SetWindowPos(Handle. HWND_TOP. Rect.Left. Rect.Top. Rect.Right - Rect.Left. Rect.Bottom - Rect.Top. SWP_SHOWWINDOW); Show; end; end else begin FForm.Borderstyle := bsDialog: FForm.ShowModal; end; if Wnd <> 0 then Result := NOERROR else Result := EJJNEXPECTED; end; function TCustomShellView.DestroyViewWindow: HResult; begin if Assigned(FForm) then FForm.Free; Fform := nil: Result := NOERROR; end; function TCustomShel1 View.GetCurrentInfо( out FolderSettings: TFolderSettings): HResult: begin Result := E_NOTIMPL; end; function TCustomShel1 View.AddPropertySheetPages( Reseved: DWORD; var IpfnAddPage: TFNAddPropSheetPage; IParam: LPARAM); HResult; begin Result ;= E_NOTIMPL; end; function TCustomShellView.SaveViewState: HResult; begi n Result ;= E_NOTIMPL; end;
Создание окон просмотра данных в Windows Explorer 401 function TCustomShel1 View.Seiectltem(pidl: PItemIDList: flags: UINT): HResult; begin Result := NOERROR: end; function TCustomShellView.GetltemObjectdtem: UINT: const iid: TIID; var IPtr: Pointer): HResult; stdcall: begin Result := E_NOTIMPL: end; {} procedure TShel1 Vi ewFactory.UpdateRegi stry( Register: Boolean): begin if Register then begin CreateRegKeyl'CLSIDV + GUIDToString(CLSID_CustomShellView)+ '\InprocServer32'. 'ThreadingModel'. 'Apartment'): inherited UpdateRegistry(Register); end else begin inherited UpdateRegistry(Register); end; end: initialization TShel 1 ViewFactory.Create(ComServer, TCustomShel1 View. CLSID_CustomShellView. ". 'View fish'. ciMultiInstance): end. Главный метод, который необходимо описать в этом интерфейсе, — метод создания окна для просмотра данных. В частности, этим окном может быть лю- бая форма со значением свойства BorderStyle, равным bsNone. На этой форме мо- гут размещаться любые элементы управления (кроме ActiveX). Подобная форма с содержимым таблицы BIOLIFE демонстрационной базы DBDEMOS реализована в модуле UTestF. Ее реализация слишком тривиальна, чтобы приводить в этой книге исходные тексты. Окно для просмотра данных реализуется при вызове метода CreateViewWindow. При этом передается ссылка на рабочую копию Windows Explorer в интерфейсе IShel 1 Browser. Создаваемая форма становится дочерним окном Windows Explorer и помещается в область, указанную в переменной Rect. Соответственно, метод DestroyViewWindow должен разрушить созданную форму. И наконец, метод UIActivate просто показывает тип активации окна просмотра в строке состояния Windows Explorer. Дополнительные данные, которые должны быть занесены в реестр, описывают модель потоков интерфейса. Они заносятся в перекрытом методе UpdateRegi stry фабрики классов TComObjectFactory.
402 Глава 9. Применение СОМ-объектов из состава Windows Данная пара модулей (вместе с модулем реализации формы) должна быть реализована в библиотеке ActiveX. После вызова команды регистрации элемента управления ActiveX в системном реестре и перезагрузки компьютера на рабочем столе Windows появляется папка Inprise Fish Viewer. Опа же будет присутство- вать и в Windows Explorer. При открытии этой папки видно содержимое формы, определенной в модуле UTestF (рис. 9.2). Рис. 9.2. Окно просмотра нефайловых данных в Windows Explorer Таким образом, мы создали средство просмотра нефайловых данных в Windows Explorer. Следующая задача, которую мы рассмотрим, будет связана с реализа- цией метода перетаскивания. Реализация метода перетаскивания Метод перетаскивания (drag-and-drop) приходится реализовывать во многих случаях, например при программировании тех или иных манипуляций с именами файлов или с иными объектами. Ниже мы обсудим, как можно реализовать этот метод с помощью СОМ. Реализация контейнера Любое приложение, которое работает с документами, должно принимать сообще- ние WM_DROPFILES Windows. Обработка этого сообщения позволяет узнать, какие файлы были выбраны пользователем в Windows Explorer, и, если файлы имеют подходящий формат, открыть их как документы. Сообщение WMJDROPFILES генери-
Реализация метода перетаскивания 403 руется при перетаскивании файла из Windows Explorer на форму, для которой предварительно была вызвана функция DragAcceptFiles Windows API. Однако реализовать перетаскивание можно и через COM-интерфейсы. Главное преиму- щество такой реализации технологии перетаскивания заключается в том, что содер- жимое выбранных файлов можно проанализировать до того, как будет отпущена кнопка мыши, и поместить на контейнер указатель мыши в виде разрешающего (или запрещающего) значка. Для реализации метода перетаскивания необходимо создать интерфейс IDropTarget: unit F11eDrop: interface uses Windows. ActiveX. Classes: type TfileDropEvent = procedure(Sender: TObject; const FileList: TStringList) of object: TfileAcceptEvent = procedure(Sender: TObject: const FileList: TStringList: var CanAccept: Boolean) of object; TfileDropAcceptor = class(TInterfacedObject. IDropTarget) private FFileList: TStringList; FOnFilesDropped: TFileDropEvent; FOnFileAccept: TFileAcceptEvent: public constructor Create(AOnDrop: TFileDropEvent: AOnEnter: TFileAcceptEvent): destructor Destroy: override: function DragEnter(const dataObj: IDataObject: grfKeyState: Longint; pt: TPoint: var dwEffect: Longint ): HResult: stdcall; function DragOverCgrfKeyState: Longint: pt: TPoint: var dwEffect: Longint): HResult: stdcall: function DragLeave: HResult: stdcall; function Drop(const dataObj: IDataObject: grfKeyState: Longint; pt: TPoint: var dwEffect: Longint ): HResult: stdcall; property OnFilesDropped: TFileDropEvent read FOnFilesDropped write FOnFilesDropped: end; implementation uses ShellAPI;
404 Глава 9. Применение СОМ-объектов из состава Windows constructor TFileDropAcceptor.Create!AOnDrop: TFi1 eDropEvent; AOnEnter: TFi1eAcceptEvent): begin inherited Create; FFileList := TStringList.Create: FOnFilesDropped ;= AOnDrop; FOnFileAccept := AOnEnter; end; destructor TFileDropAcceptor.Destroy: begin FFileList.Free: inherited Destroy: end; function TFileDropAcceptor.DragEnter(const dataObj: IDataObject: grfKeyState: Longint; pt: TPoint: var dwEffect: Longint): HResult; var Medium: TSTGMedium: Format: TFormatETC: NumFiles: Integer: I: Integer: rslt: Integer; FileName: array [0.,MAX_PATH] of char: S: String; CanDrop: Boolean: begin dataObj._AddRef; Format.cfFormat : = CF_HDROP; Format.ptd : = nil: Format.dwAspect : = DVASPECT_CONTENT: Format.1index := -1; Format.tymed := TYMED_HGLOBAL; rslt := dataObj.GetData!Format. Medium): FFileList.Cl ear: if rslt=S_OK then begin NumFiles := DragQueryFile(Medium.hGlobal. SFFFFFFFF. nil. 0); for I := 0 to NumFiles - 1 do begin DragQueryFile(Medium.hGlobal, I. FileName. SizeOf(FileName)); S ;= FileName: FFileList.Add(S): end; end:
Реализация метода перетаскивания 405 if Medium.unkForRelease = nil then ReleaseStgMedium(Medium): dataObj._Release: CanDrop := False: if FFileList.Count > 0 then begin CanDrop := True: if Assigned(FOnFileAccept) then FOnFileAccept(Self. FFileList. CanDrop): end: if CanDrop then dwEffect := DROPEFFECT_COPY else begin FFileList. Clear; dwEffect := DROPEFFECT_NONE: end: Result := S_OK: end: function TF11eDropAcceptor.DragOver( grfKeyState: Longint: pt: TPoint; var dwEffect: Longint): HResult: begin if FFileList.Count > 0 then dwEffect := DROPEFFECT_COPY else dwEffect := DROPEFFECT_NONE; Result := S_OK; end: function TFileDropAcceptor.DragLeave: HResult; begin Result := S_OK: end: function TFileDropAcceptor.Drop(const dataObj: IDataObject: grfKeyState: Longint: pt: TPoint; var dwEffect: Longint): HResult; begin if Assigned(FOnFilesDropped) and (FFileList.Count > 0) then begi n FOnFi1esDropped(Self. FFi1eList): dwEffect := DROPEFFECT_COPY; end else dwEffect := DROPEFFECT_NONE; Result := S_OK; end; initialization
406 Глава 9. Применение СОМ-объектов из состава Windows Oleinitialize(nil): finalization 01 eUninitialize; end. Интерфейс IDnopTanget реализуется в классе TFi1eDropAcceptor — потомке класса TIntenfacedObject — и поэтому не требует создания фабрики классов и регист- рации в системном реестре. Его конструктор содержит два метода: первый — FOnFilesDropped — вызывается, когда пользователь отпускает кнопку мыши на кон- тейнере, а второй — FOnFileAccept — когда указатель мыши попадает на контей- нер. Оба эти метода передают список файлов, выделенных в Windows Explorer. В обработчике события FOnFileAccept необходимо проанализировать список файлов, попытаться их открыть и определить, подходящий ли у них формат. Переменную CanAccept следует установить равной True, если файлы (файл) име- ют подходящий формат, и False — если нет. Обработчик второго события — FOnFi 1 eDropped — будет вызываться, только если переменная CanAccept была уста- новлена равной True в обработчике событий FonFi leAccept или если этот обра- ботчик отсутствует. Соответственно, в нем следует произвести все манипуляции с выбранными файлами. Для передачи окружению указателя на созданный интерфейс вызывается функция RegisterDragDrop, которая находится в модуле ActiveX. Перед разруше- нием контейнера необходимо вызвать функцию RevokeDragDrop. Пример кода для контейнера приведен ниже: unit DTForm; interface uses Windows. Messages. SysUtils, Classes. Graphics. Controls. Forms. StdCtrls. FileDrop; type TForml = class(TForm) ListBoxl: TListBox: procedure FormCreate(Sender: TObject): procedure FormClosetSender: TObject: var Action: TCloseAction); private procedure OnFilesDroppedCSender: TObject: const FileNames: TStringList); procedure OnDragEnterCSender: TObject; const FileNames: TStringList: var CanEnter: Boolean); end: var Forml: TForml;
Реализация метода перетаскивания 407 implementation uses ActiveX: {$R *.DFM} procedure TForml.FormCreate(Sender: TObject): var FDropAcceptor: TFi1eDropAcceptor; begin FdropAcceptor := TFi1eDropAcceptor.Create( OnFilesDropped. OnDragEnter); Regi sterDragDrop(Li stBoxl.Handl e. FDropAcceptor as IDropTarget); end: procedure TForml.FormClose(Sender: TObject: var Action: TCIoseAction): begin RevokeDragDrop(Li stBoxl.Handle): end: procedure TForml.OnFilesDropped(Sender: TObject: const FileNames: TStringList): begin L i stboxl.Iterns.As s i gn(Fi1 eNames): end; procedure TForml.OnDragEnter(Sender: TObject: const FileNames: TStringList; var CanEnter: Boolean); begin CanEnter := FileNames.Count > 1; end; end. Следует обратить внимание на то, что деструктор объекта EDropAcceptor, в кото- ром реализован интерфейс IDropTarget, нигде не вызывается. Это сделано потому, что при вызове метода RevokeDragDrop происходит разрушение данного объекта. Соответственно, нет необходимости хранить ссылку на этот объект, и поэтому он объявлен как локальная переменная. Реализация источника данных С помощью технологии СОМ можно реализовать и обратную операцию, а именно превратить приложение в источник данных для операции перетаскивания. Соот- ветственно, такие объекты могут быть переданы из Windows Explorer для переме- щения, создания копий и ярлыков и т. д.
408 Глава 9. Применение СОМ-объектов из состава Windows Для реализации источника данных требуется описание трех интерфейсов — IDropSource, IDataObject и lEnumFormatEtc. Интерфейс IDropSource определяет тип указателя мыши, возникающего при перетаскивании файлов, и информирует об окончании или продолжении операции перетаскивания. Интерфейс IDataObject подготавливает список файлов в специальном формате, который определяется в структуре TDropFiles. Эти данные передаются по требованию приложению-кли- енту, над окном которого была отпущена кнопка мыши. И наконец, интерфейс lEnumFormatEtc используется интерфейсом IDataObject для получения информации от клиента о том, какие именно форматы им поддерживаются. Для перетаскивания файлов в интерфейсе IDataObject необходимо реализовать поддержку единственного формата — CF_HDROP. Абсолютно все методы интерфей- сов IDropSource и lEnumFormatEtc должны быть работоспособными — ни один из них не должен возвращать E_NOTIMPL — константу, которая указывает, что интер- фейс не поддерживает вызываемый метод. Что касается интерфейса IDataObject, то достаточно реализовать три его метода — GetData, QueryGetData и EnumFormatEtc. Все остальные методы могут возвращать константу E NOTIMPL. Ниже приведен текст модуля, содержащий реализацию всех трех интерфейсов: unit DataObj: 1nterface uses Windows. ActiveX. Classes: type TdataRequestNotify = procedure(const DataList: TStringList: IsFirstTime: Boolean) of object: TfileSource = class(TInterfacedObject. IDropSource) function QueryContinueDrag(fEscapePressed: BOOL; grfKeyState: Longint): HResult; stdcall: function GiveFeedback(dwEffect: Longint): HResult: stdcal1; end: TdataObject = c1ass(TInterfacedObject. IDataObject) private FFileList: TStringList: FDataRequest: TDataRequestNoti fy; FIsFirstTime: Boolean: FDropPoint: TPoint: FInClient: Boolean: function StorageSize: Integer: public constructor Create!const AFileList: TStringList; ADataRequest: TDataRequestNotify: DropPoint: TPoint: InClient: Boolean); destructor Destroy; override;
Реализация метода перетаскивания 409 { IDataObject } function GetData(const formatetcln: TFormatEtc: out medium: TStgMedium): HResult: stdcall: function GetDataHerelconst formatetc: TFormatEtc: out medium: TStgMedium): HResult; stdcall: function QueryGetData(const formatetc: TFormatEtc): HResult: stdcall; function GetCanonicalFormatEtc(const formatetc: TFormatEtc; out formatetcOut: TFormatEtc): HResult: stdcall: function SetData(const formatetc: TFormatEtc: var medium: TStgMedium; fRelease: BOOL): HResult: stdcall; function EnumFormatEtc(dwDirecti on: Longint: out enumFormatEtc: lEnumFormatEtc): HResult; stdcall; function DAdvise(const formatetc: TFormatEtc: advf: Longint: const advSink: lAdviseSink: out dwConnection: Longint): HResult: stdcall: function DUnadvise(dwConnection: Longint): HResult: stdcall; function EnumOAdvise(out enumAdvise: lEnumStatData): HResult: stdcall: end: implementation uses ShiObj. SysUtils: const DataFormatCount = 1: type PformatList = ^TFormatList: TformatList = array[O..DataFormatCount-1] of TFormatEtc; var DataFormats: TFormatList: type TEnumFormatEtc = class(TInterfacedObject. lEnumFormatEtc) private FFormatList: PFormatList: FFormatCount: Integer: FIndex: Integer: public constructor CreateCFormatList: PFormatList: FormatCount. Index: Integer): function Nextlcelt: Longint; out elt: pceltFetched: PLongint): HResult: stdcall:
410 Глава 9. Применение СОМ-объектов из состава Windows function SkipCcelt: Longint): HResult: stdcall: function Reset: HResult; stdcall: function Clone(out enum:lEnumFormatEtc): HResult; stdcall: end: function TF11eSource.QueryContinueDrag(fEscapePressed: BOOL: grfKeyState: Longint): HResult: begi n if fEscapePnessed then Result := DRAGDROP_S_CANCEL else if (gnfKeyState and MK_LBUTTON) = 0 then Result := DRAGDROP_S_DROP else Result := S_OK; end: function TFileSource.GiveFeedback( dwEffect: Longint):HResult; begin case dwEffect of DROPEFFECT_NONE. DROPEFFECT_COPY. DROPEFFECT_LINK. DROPEFFECT_SCROLL: Result := DRAGDROP_S_USEDEFAULTCURSORS: else Result := S_OK; end; end: {} constructor TEnumFormatEtc.Create(ForniatList: PFormatList: FormatCount. Index: Integer): begin inherited Create: FFormatList := FormatList: FFormatCount := FormatCount: FIndex := Index; end: function TEnumFormatEtc.Next(celt: Longint: out elt: pceltFetched: PLongint); HResult; var I: Integer; begin I := 0; while (I < celt) and (FIndex < FFormatCount) do begin TFormatList(elt)[I] ;= FFormatList[FIndex]; Inc(FIndex); Inc(I); end:
Реализация метода перетаскивания 411 if pceltFetched <> nil then pceltFetched* := I: if I = celt then Result := S_OK else Result := S_FALSE: end: function TEnumFormatEtc.Skip(celt: Longint): HResult: begin if celt <= FFonmatCount - FIndex then begin FIndex := FIndex + celt: Result := S_OK: end else begin FIndex := FFonmatCount; Result : = S_FALSE: end: end; function TEnumFormatEtc.Reset: HResult: begin FIndex := 0: Result := S_OK; end: function TEnumFonmatEtc.Clone(out enum: lEnumFonmatEtc): HResult; begin enum := TEnumFormatEtc.Create!FFormatList. FFonmatCount. FIndex): Result := S_OK; end: {} constructor TDataObject.CreateCconst AFileList: TStringList: ADataRequest: TDataRequestNotify: DropPoint: TPoint: InClient: Boolean); begin inherited Create: FFileList := TStringList.Create; FFileList.Assign(AFileList): FDataRequest := ADataRequest: FIsFirstTime := True; FDropPoint := DropPoint: FInClient := InClient; end: destructor TDataObject.Destroy; begin if Assigned(FFileList) then FFileList.Free: inherited Destroy: end:
412 Глава 9. Применение СОМ-объектов из состава Windows function TDataObject.StorageSize: Integer; var I: Integer; begin Result ;= SizeOf(TDropFiles): {Double-terminated null} if FFileList.Count > 0 then for I := 0 to FFileList.Count - 1 do Result := Result + Length(FFi1eList[I]) + 1; Inc(Result); end; function TDataObject.GetData(const formatetcln: TFormatEtc; out medium: TStgMedium): HResult; var Data: HGlobal; P; PChar: I. N: Integer; DF: PDropFiles: S: String; begin Result := DV_E_FORMATETC; medium.tymed := 0; medium.hGlobal := 0; mediurn.unkForRelease := nil: with formatetcln do if (cfFormat = CF_HDROP) and (dwAspect = DVASPECT-CONTENT) and (tymed = TYMED_HGLOBAL) then begin if Assigned(FDataRequest) then FDataRequest(FFileList. FIsFirstTime): FIsFirstTIme := False: Data := Global Alloc(GMEM_SHARE or GMEM_MOVEABLE or GMEM_ZEROINIT. StorageSize); if (Data <> 0) and (FFileList.Count > 0) then begin DF := GlobalLock(Data): if Assigned(DF) then begin DF.pFiles := SizeOf(TDropFiles); DF.pt := FDropPoint; DF.fNC := FInClient; DF.fWide := False; P := PChar(DF); N := SizeOf(TDropFiles); for I := 0 to FFileList.Count - 1 do begin S := FFileListLI]; System.Move(S[l], P[N], Length(S)); N ;= N + Length(S) + I; end;
Реализация метода перетаскивания 413 GlobalUnlock(Data): end: medium.tymed := TYMED_HGLOBAL; medium.hGlobal := Data: Result := S_OK; end; end: end; functi on TDataObject.GetDataHere( const formatetc: TFormatEtc; out medium: TStgMedium): HResult: begin Result := DV_E_FORMATETC: end: function TDataObject.QueryGetData( const formatetc: TFormatEtc): HResult; begin Result := DV_E_FORMATETC: with formatetc do if dwAspect = DVASPECT_CONTENT then if (cfFormat = CF_HDROP) and (tymed = TYMED_HGLOBAL) then Result := S_OK; end: function TDataObject.GetCanonicalFormatEtc( const formatetc: TFormatEtc: out formatetcOut: TFormatEtc): HResult: begin formatetcOut.ptd := nil; Result := E_N0TIMPL: end: function TDataObject.SetData(const formatetc: TFormatEtc; var medium: TStgMedium; fRelease: BOOL): HResult: begin Result := E_NOTIMPL: end: function TDataObject.EnumFormatEtc(dwDirection: Longint: out enumFormatEtc: lEnumFormatEtc); HResult; begin if dwDirection = DATADIR_GET then begin enumFormatEtc := TEnumFormatEtc.Create(@DataFormats. DataFormatCount, 0):
414 Глава 9. Применение СОМ-объектов из состава Windows Result := S_OK; end else begin enumFormatEtc : = nil: Result E_NOTIMPL: end: end: function TDataObject.DAdvise(const formatetc: TFormatEtc; advf: Longint: const advSink: lAdviseSink: out dwConnection: Longint): HResult; begin Result := OLE_E_ADVISENOTSUPPORTED: end: function TDataObject.D(Jnadvise( dwConnection: Longint): HResult: begin Result := OLE_E_ADVISENOTSUPPORTED: end: function TDataObject.EnumDAdvise( out enumAdvise: lEnumStatData): HResult: begin Result := OLE_E_ADVISENOTSUPPORTED: end: {} initialization DataFormats[0].cfForniat := CF_HDROP; DataFormats[0].ptd := nil: DataFormats[0].dwAspect := DVASPECTCONTENT: DataFormats[0].lIndex := -1; DataFormats[0].tynied := TYMED_HGLOBAL: Olelnitialize(nil); finalization OleUnlnitialize: end. Метод QueryContinueDrag интерфейса IDropSource вызывается всякий раз, когда происходит изменение в состоянии специальных клавиш клавиатуры (например, Esc) и/или кнопок мыши. Возможно возвращение одного из трех значений: Ш DRAGDROP_S_CANCEL — прерывание операции перетаскивания; № DRAGDROP_S_DROP — окончание операции перетаскивания и начало копирования данных; № S_OK — продолжение операции перетаскивания.
Реализация метода перетаскивания 415 Метод GiveFeedback принимает в качестве параметра информацию о том, как целевой элемент управления хочет распорядиться данными. Он должен вер- нуть тип указателя мыши для ожидаемой операции — обычно это DRAGDROP_S_ USEDEFAULTCURSORS. Метод Next интерфейса lEnumFormatEtc возвращает список форматов, начиная с текущей позиции внутреннего счетчика FIndex. Требуемое их число содержит- ся в переменной celt. Формат этого списка приведен в структуре TFormatList. Эти данные помещаются в переменную el t, место в памяти для которой должно зарезервировать клиентское приложение. И наконец, в указателе pceltFetched помещается суммарное число форматов, записанных в переменной el t (если ука- затель идентифицирует какую-либо область в памяти). Метод Skip перемещает внутренний счетчик FIndex на elt записей вперед. Метод Reset устанавливает внутренний счетчик на первый элемент в списке форматов, поддерживаемый данным интерфейсом. И наконец, метод Cl one создает копию интерфейса с теку- щим значением внутреннего счетчика. Конструктор объекта, реализующего интерфейс IDataObject, принимает в ка- честве параметра список выбранных файлов, координаты точки и флаг, указы- вающий, должен ли находиться указатель мыши в клиентской области элемента управления, над которым он проходит, или нет. Все эти данные будут переда- ваться клиенту в структуре TDropFiles. Однако чаще всего клиент не анализирует значения координат точки и флага fNC. Кроме того, в конструктор передается ад- рес нотификационного сообщения, которое будет вызываться всякий раз, когда интерфейс IDataObject передает данные клиенту. Это сообщение можно использо- вать, например, чтобы создать файлы на носителе, если они ранее не были созданы. Поэтому нотификационное сообщение содержит еще один параметр — IsFi rstTime, указывающий, первый раз или нет оно вызывается из данной копии объекта TDataObject. Метод GetData создает структуру TDropFi 1 es в памяти и заполняет ее. Струк- тура TDropFiles содержит заголовок, а за ним — имена выбранных файлов и пути к ним. Они разделяются символами #00, и после последнего имени файла обя- зано находиться два таких символа. Данный метод проверяет, запрашивается ли поддерживаемый формат, и если он поддерживается, то возвращает значение S_OK, в противном случае — DV_E_FORMATETC. И, наконец, метод EnumFormatEtc возвращает интерфейс lEnumFormatEtc. Этот же метод может быть использован для добавления новых форматов к списку фор- матов интерфейса IDataObject (данная возможность в этом примере не поддер- живается). Ниже приведен исходный код модуля, реализующий источник данных OLE: unit DSForm: interface uses Windows. Messages. SysUtils. Classes. Graphics, Controls. Forms. Dialogs, StdCtrls:
416 Глава 9. Применение СОМ-объектов из состава Windows type TForml = class(TForm) LBFiles: TListBox: Label1: TLabel; procedure LBFilesMouseDown(Sender: TObject: Button: TMouseButton: Shift: TShiftState: X. Y: Integer); procedure LBFilesMouseUp(Sender: TObject: Button: TMouseButton: Shift: TShiftState; X, Y: Integer): procedure LBFilesMouseMove(Sender: TObject: Shift: TShiftState: X. Y: Integer): procedure FormCreatelSender: TObject): private DragPoint: TPoint: FDragStarted: Boolean: procedure GetDataNotifylconst DataList: TStringList: IsFirstTime: Boolean): end: var Forml: TForml: implementation uses DataObj. ComObj. ActiveX: {$R *.DFM} procedure TForml.LBFilesMouseDown(Sender: TObject; Button: TMouseButton: Shift: TShiftState; X. Y: Integer): begin if Button = mbLeft then begin OragPoint.X := X; DragPoint.Y := Y: end; end; procedure TForml.LBFilesMouseUp(Sender: TObject: Button: TMouseButton; Shift: TShiftState: X. Y: Integer); begin if Button = mbLeft then begin DragPoint.X := Integer($FFFFFFFF): DragPoint.Y : = Integer($FFFFFFFF): end; end:
Реализация метода перетаскивания 417 procedure TForml.LBF11esMouseMove(Sender: TObject; Shift; TShiftState: X. Y; Integer); var FileSource: TFileSource; DataObject: TDataObject: DP: TPoint: dwEffect: DWORD: FileList: TStringList; I: Integer: CD.S: String: begin if FDragStarted then Exit: if (ssLeft in Shift) and (DragPoint.X <> Integer($FFFFFFFF)) and (DragPoint.Y <> Integer($FFFFFFFF)) then if (Abs(X - DragPoint.X) + Abs(Y - DragPoint.Y)) > 10 then begin FDragStarted := True: DragPoint.X := Integer!$FFFFFFFF): DragPoint.Y := Integer($FFFFFFFF); Fi 1 eList := nil: try FileList := TStringList.Create; GetDir(0, CD): for I := 0 to LBFiles.Items.Count - 1 do if LBFiles.Selected[I] then begin S := CD + '\' + LBFiles.ItemsEI]; FileList.Add(S): end: if FileList.Count > 0 then begin FileSource := TFileSource.Create: DP.x := 100: DP.у := 100; DataObject := TDataObject.Create(FileList. GetDataNotify. DP. True); 01eCheck(DoDragDrop(DataObject as IDataObject. FileSource as IDropSource. DROPEFFECT_COPY. dwEffect)): end; finally FileList.Free: // He нужно уничтожать интерфейсы IDataObject и 11 IDropSource - они будут уничтожены автоматически FDragStarted := False: end; end: end:
418 Глава 9. Применение СОМ-объектов из состава Windows procedure TForml.GetDataNotify(const DataList: TStringList: IsFirstTime: Boolean); var F: TextFile; I: Integer; begin if not IsFirstTime then Exit; if Assigned(DataList) and (DataList.Count > 0) then for I ;= 0 to DataList.Count - 1 do begin AssignFile(F, DataList[I]); Rewrite(F); Writeln(F. ExtractFileName(DataList[I])); CloseFile(F); end: end; procedure TForml.FormCreateCSender: TObject): var CD. S: String; I: Integer: begin GetDir(0. CD); for I := 0 to LBFiles.Items.Count - 1 do if LBFiles.Selected[I] then begin S := CD + ' \' + LBFiles.ItemstU; DeleteFi1e(S): end: end; end. Данный модуль связан с формой, на которой размещен компонент TListBox со списком файлов от 1.txt до 6.txt. В списке можно выделить несколько строк (рис. 9.3). "Ж Filename list 1.txt ztxt 3.txt 4.txt 5. txt 6. txt The lies ae rot exult at the <&tc They ar-cieet'.d on request ard contars snisic Ine win r*.-Ther. equal tn lilenarre Рис. 9.3. Источник данных для операции перетаскивания Внутренняя переменная DragPoint хранит сведения о точке, на которой была нажата левая кнопка мыши. Если указатель мыши с нажатой кнопкой был сме- щен более чем на 10 пикселов, то в обработчике события OnMouseMove начинается
Использование Microsoft Internet Explorer в приложениях 419 операция по перемещению файлов. При этом в переменной FDragStarted выстав- ляется флаг, указывающий, что перемещение начато, а также создается объект Fi 1 eLi st, куда помещаются все выделенные в списке названия файлов, к которым добавляется путь, соответствующий текущему каталогу. После этого создаются экземпляры объектов, реализующих интерфейсы IDropSource и IDataObject. Далее происходит вызов метода DoDragDrop, который в качестве параметров принимает ссылки на данные интерфейсы и выполняет OLE-метод перетаскивания. После окончания работы метода DoDragDrop объект FileList разрушается. Следует обра- тить внимание на то, что явно не вызываются деструкторы объектов, поддержи- вающих интерфейсы. Эти деструкторы вызываются автоматически после завер- шения операции перетаскивания. Во время операции перетаскивания интерфейс IDataObject вызывает метод GetDataNotify. В этом методе, если он был вызван в первый раз, создаются выбран- ные файлы. И наконец, при создании формы файлы удаляются с диска, если они ранее там находились. Использование Microsoft Internet Explorer в приложениях Во многих современных программах требуется работа с данными в формате HTML. В качестве средства для просмотра таких данных в Delphi используется компонент TWebBrowser (являющийся элементом управления ActiveX), который задействует компонент WebBrowser, входящий в состав Microsoft Internet Explorer. Таким обра- зом, он имеется на любом компьютере, на котором установлен браузер Internet Explorer. Все последние версии Windows содержат этот компонент в стандартном комплекте поставки и, более того, практически неработоспособны без него. Базовые операции Для того чтобы использовать Microsoft Internet Explorer в своей программе, необ- ходимо разместить на форме компонент TWebBrowser, находящийся на странице Internet палитры компонентов. Чтобы после этого отобразить в нем HTML-стра- ницу, необходимо вызвать его метод Navigate: procedure TForml.ButtonlClick(Sender: TObject): var Flags. TargetFrameName. PostData. Headers: OleVariant; begin WebBrowserl.Navigate('http://www.borland.com'. Flags. TargetFrameName. PostData. Headers): end: Рассмотрим подробнее параметры, передаваемые методу Navigate. Первым параметром передается строка с URL-адресом, с которого должна осуществляться загрузка. Поддерживаются все протоколы, доступные в Internet Explorer, например file:// — загрузка файла, res:// — загрузка ресурса.
420 Глава 9. Применение СОМ-объектов из состава Windows Остальные параметры не являются обязательными и служат для передачи до- полнительной информации. » Flags — целое число, представляющее из себя битовую маску из следующих флагов: □ 1 — открыть ресурс в новом окне; □ 2 — не добавлять страницу в список просмотренных страниц; □ 4 — не загружать страницу из кэша; □ 8 — не сохранять страницу в кэше. ж TargetFrameName — задает имя фрейма, в который будет загружена страница. № PostData — задает данные для запроса с сервера методом POST. Если этот пара- метр не задан, используется метод GET. № Headers — задает дополнительные заголовки HTTP. Наиболее интересным является параметр PostData, позволяющий передать не web-сервер данные, полученные в результате заполнения формы, если этот сервер требует HTTP-транзакции методом POST. Например, следующий фрагмент коде передает на сервер имя пользователя и пароль, введенные в форме Delphi: var LoginDialog: TLoginDia 1og: Flags. TargetFrameName. PostData. Headers: OleVariant: S: String: with TLoginDialog.Create(Application) do try if ShowModal = mrOk then begin S := Format!'UserName=fc&Password=^s'. [Editl.Text. Edit2.Text]); PostData := VarArrayCreate([l. Length(S) + 1]. varByte): System.Move(S[l], VarArrayLock(PostData)*. Length(S) +1): VarArrayUnlock(PostData): Headers := 'Content-Type: application’* '/x-www-form-urlencoded’#13#10: WebBrowser!.Navi gate( 'http://intranetserver/secretpage', Flags. TargetFrameName. PostData. Headers): end: finally Free; end; Ha web-сервере этот запрос может быть обработан, например, следующи; ASP-сценарием: Dim sConnect Dim sUserName Dim sPassword
Использование Microsoft Internet Explorer в приложениях 421 slIserName = Request.Form!"User") sPassword = Request.Form!"Pass") sConnect = "Provider=SQLOLEDB.l:" & _ "Persist Security Info=True:" & _ "Initial Catalog=Katren:Data Source=DBSERVER:" & _ "Password=" & sPassword & _ ":User ID=" & sUserName SessionU'ConnectString") = sConnect После того как данные получены, необходимо предоставить пользователю возможность работы с ними. Многие функции компонента TWebBrowser доступны через метод ExecWB, предоставляющий простой способ обращения к интерфейсу lOleCommandTarget. Этот метод имеет вид: procedure TWebBrowser.ExecWB( cmdID: OLECMDID: 11 идентификатор команды cmdexecopt: OLECMDEXECOPT; // параметры выполнения var pvaln. // дополнительные параметры, pvaOut: 01 eVari ant // зависящие от команды ): safecall: Параметры метода перечислены ниже. Ж cmdID — может быть одной из констант OLECMDID, определенных в файле ShDocVw.pas. И cmdexecopt — может принимать одно из следующих четырех значений: □ OLECMDEXECOPT_DODEFAULT — выполнить команду с настройками по умолча- нию; □ OLECMDEXECOPT_PROMPTUSER — запросить у пользователя настройки для выпол- нения команды (например, при печати — вывести диалоговое окно Print Setup); □ OLECMDEXECOPT_DONTPROMPTUSER — выполнить команду, не запрашивая пользо- вателя; □ OLECMDEXECOPT_SHOWHELP — вывести справку о команде. Ж Параметры pvaln и pvaOut являются дополнительными и зависят от конкрет- ной команды. Имеется возможность запросить у компонента TWebBrowser доступность той или иной команды при помощи следующей функции: function TWebBrowser.QueryStatusWBC cmdID: OLECMDID // идентификатор команды ): OLECMDF: safecall: Функция возвращает битовую маску из следующих значений: Я OLECMDF_SUPPORTED — команда поддерживается; Ж OLECMDF_ENABLED — команда поддерживается и доступна;
422 Глава 9. Применение СОМ-объектов из состава Windows Я OLECMDF_LATCHED — команда представляет собой переключатель и сейчас вклю- чена; в OLECMDF_NINCHED — зарезервировано. Таким образом, можно настраивать интерфейс в зависимости от поддержи- ваемых текущей версией TWebBrowser возможностей. Для печати содержимого TWebBrowser служит команда OLECMDID_PRINT. Метод печати может выглядеть, например, следующим образом: procedure TForml.ActionPrintExecute(Sender: TObject); var A. B: OleVariant; UserAction: Cardinal; begin if Sender = ActionPrintWithSetup then UserAction := OLECMDEXECOPT_PROMPTUSER el se UserAction := OLECMDEXECOPT_DONTPROMPTUSER; try WebBrowserl.ExecWB(OLECMDID_PRINT. UserAction. A. B); except end; end: Блок try...except...end необходим, поскольку компонент TWebBrowser при вы- полнении любой команды при помощи метода ExecWB генерирует исключение Е01 eExcepti on со следующим кодом: -2147221248 ($80040100) Trying to revoke a drop target that has not been regi stered Некоторые команды (например, печать, копирование в буфер обмена) доступны через интерфейс lOleCommandTarget объекта WebBrowser.Document. Описанная выше процедура печати текущей страницы при помощи интерфейса lOLECommandTarget выглядит следующим образом: procedure TForml.ActionPrintExecute(Sender: TObject); var pvain. pvaOut: OLEVariant: IT: lOleCommandTarget; begin if Printer = nil then begin MessageDlg(PrinterNotInstalled, mtError. [mbOK], 0); Exit; end; pvain := Unassigned; pvaout ;= Unassigned; IT WEBBrowser.Control Interface as lOleCommandTarget; OleCheck(IT.Exec(nil. OLECMDID_PRINT. 0, pvain. pvaOut)); end;
Использование Microsoft Internet Explorer в приложениях 423 Обратите внимание на отсутствие блока try...except...end — при таком способе печати текущей страницы не происходит генерация исключения. Применение интерфейса lOLECommandTarget для копирования данных в буфер обмена выглядит следующим образом: var CmdTarget: lOleCommandTarget; vain. vaOut: OleVariant: begin if Assigned(WebBrowser.Document) then begin CmdTarget := WebBrowser.Document as lOleCommandTarget; 01 eCheck(CmdTarget.Exec(nil. OLECMDID_COPY, OLECMDEXECOPT_DODEFAULT. vain, vaOut)); end: end: При помощи этого же интерфейса можно получить информацию о доступно- сти команд: var HasOocument: Boolean; CmdTarget: lOleCommandTarget; Cmds: array[O..O] of OleCmd; begin HasOocument := Assigned(WebBrowser.Document); if HasDocument then begin CmdstOJ.cmdIO : = OLECMDID_COPY; CmdTarget := WebBrowser.Document as lOleCommandTarget: 01 eCheck(CmdTarget.QueryStatus(nil. SizeOf(Cmds) div SizeOf(Cmds[0]), @Cmds. nil)): acCopy.Enabled := Cmds[O].cmdf and OLECMDF_ENABLED <> 0; end else begin acCopy.Enabled := False: end; end; Начиная c Internet Explorer 5, документированы дополнительные команды, поддерживаемые через интерфейс lOleCommandTarget. Они существенно расширяют возможности по управлению компонентом, однако недоступны либо не докумен- тированы в версии Internet Explorer 4. Это создает определенные сложности при программировании. Например, чтобы организовать поиск внутри загруженной страницы, необходим следующий код: const // Недокументированная константа CGI0JE4: TGUID = '{ed016940-bd5b-llcf-ba4e-00c04fd70816}'; // Документировано в IE5 SDK
424 Глава 9. Применение СОМ-объектов из состава Windows CGID_MSHTML: TGUID = '{DE4BA900-59СА-11CF-9592-444553540000}'; IDM_FIND = 67: procedure TForml.ActionFindExecute(Sender: TObject): var A. B: OleVariant: Target: lOleCommandTarget: OleCmd: TOLECMD: begin // Получаем интерфейс lOleCommandTarget Target := wbMain.Document as lOLECommandtarget; with OleCmd do begin cmdld := IDM_FIND: cmdf := 0: end: // Запрашиваем, поддерживается ли команда Target.QueryStatus(@CGID_MSHTML. 1. OOleCmd. NIL): if (OleCmd.cmdf and OLECMDF_SUPPORTED) = OLECMDF_SUPPORTED then // IE5+ - используем документированный способ Target.Exec(@CGID_MSHTML. IDM_FIND. OLECMDEXECOPT_DODEFAULT, A, B) el se // IE4 - используем недокументированный способ Target.Exec(@CGID_IE4. 1. OLECMDEXECOPT_DODEFAULT, A. B); end: Использование недокументированного вызова в данном случае оправдано, так как в версии 4 этот вызов уже не будет изменяться, а в версии 5 мы обнару- живаем и используем документированный метод. В то же время браузер Internet Explorer 4 еще достаточно распространен, и совсем лишать программу возмож- ности поиска на таких компьютерах нельзя. Тонкая настройка Для более тонкой настройки компонента TWebBrowser необходимо реализовать интерфейс IDocHostUIHandler, позволяющий программисту взять под контроль по- ведение этого компонента. Интерфейс объявляется следующим образом: type TDocHostlnfo = packed record cbSize: ULONG: dwFlags: DWORD; dwDoubleClick: DWORD: end: const DOCHOSTUIFLAG_DIALOG = 1:
Использование Microsoft Internet Explorer в приложениях 425 DOCHOSTUIFLAG_DISABLE_HELP_MENU = 2; DOCHOSTUIFLAG_NO3DBORDER = 4: DOCHOSTUIFLAG_SCROLL_NO = 8: DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE = 16: DOCHOSTUIFLAGJ3PENNEWWIN = 32; DOCHOSTUIFLAG_DISABLE_OFFSCREEN = 64: DOCHOSTUIFLAG_FLAT_SCROLLBAR = 128; DOCHOSTUIFLAG_DIV_BLOCKDEFAULT = 256: DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY = 512; const DOCHOSTUIDBLCLK_DEFAULT = 0: DOCHOSTUIDBLCLK_SHOWPROPERTIES = 1; DOCHOSTUIDBLCLK_SHOWCODE = 2; type IDocHostUIHandler = interface(IUnknown) [’{bd3f23c0-d43e-llcf-893b-00aa00bdcela}'] function ShowContextMenu(const dwID: DWORD; const ppt: PPOINT; const pcmdtReserved: IUnknown: const pdispReserved: IDispatch): HRESULT; stdcall: function GetHostlnfoI var plnfo: TDOCHOSTUIINFO): HRESULT; stdcall; function ShowUI(const dwID: DWORD; const pActiveObject; lOlelnPlaceActiveObject: const pCommandTarget: lOleCommandTarget: const pFrame: lOlelnPlaceFrame; const pDoc: IDlelnPlaceUIWindow): HRESULT; stdcall: function HideUI: HRESULT; stdcall; function UpdateUI; HRESULT: stdcall: function EnableModeless(const fEnable: BOOL): HRESULT: stdcall; function OnDocWindowActivate(const fActivate: BOOL): HRESULT: stdcall: function OnFrameWindowActivateCconst fActivate: BOOL): HRESULT: stdcall: function ResizeBorder(const preBorder: PRECT: const pUIWindow: IDlelnPlaceUIWindow; const fRameWindow: BOOL): HRESULT; stdcall: function TranslateAccelerator(const IpMsg: PMSG: const pguidCmdGroup: PGUID: const nCmdlD: DWORD): HRESULT; stdcall; function GetOptionKeyPath(var pchKey: POLESTR: const dw: DWORD): HRESULT: stdcall: function GetDropTargetCconst pDropTarget: IDropTarget: out ppDropTarget: IDropTarget): HRESULT: stdcall; function GetExternaKout ppDispatch: IDispatch):
426 Глава 9. Применение СОМ-объектов из состава Windows HRESULT: stdcall: function TranslateUrl(const dwTranslate: DWORD: const pchURLIn: POLESTR: var ppchURLOut: POLESTR): HRESULT; stdcall: function FilterDataObject(const pDO: IDataObject: out ppDORet: IDataObject): HRESULT; stdcall; end: Наследник класса TWebBrowser, реализующий этот интерфейс, должен быть объявлен так: type TCustomizedWebBrowser = class(TWebBrowser. IDocHostUIHandler) // Реализация методов IDocHostUIHandler end: Код такого компонента, обладающего минимальной функциональностью, и при- мер использующей его программы приведены на прилагаемом компакт-диске. Вы можете использовать этот код как основу для создания своих наследников класса TWebBrowser с расширенными возможностями (рис. 9.4). Рис. 9.4. Пример использования компонента TWebBrowser Рассмотрим наиболее интересные с точки зрения программиста методы интер- фейса IDocHostUIHandler. function ShowContextMenuCconst dwID: DWORD; const ppt: PPOINT; const pcmdtReserved: IUnknown: const pdispReserved: IDispatch): HRESULT:
Использование Microsoft Internet Explorer в приложениях 427 Эта функция вызывается, когда компонент TWebBrowser должен показать кон- текстное меню. Если вы отображаете собственное меню либо хотите подавить вывод меню — функция должна вернуть значение S_OK, если меню должен пока- зать компонент TWebBrowser — значение S_FALSE. В функцию ShowContextMenu передаются следующие параметры: ж DwID — идентификатор меню, который может принимать одно из перечислен- ных ниже значений (в зависимости от значения идентификатора вы можете вывести подходящее меню): const CONTEXT_MENU_DEFAULT = 0: CONTEXT_MENU_IMAGE = 1: CONTEXT_MENU_CONTROL = 2: CONTEXT_MENU_TABLE = 3: CONTEXT_MENU_DEBUG = 4: CONTEXT_MENU_1DSELECT = 5: CONTEXT_MENU_ANCHOR = 6: CONTEXT_MENU_IMGDYNSRC = 7; Ж ppt — координаты, в которых должно быть показано меню; Я pcmdtReserved — интерфейс lOleCommandTarget, позволяющий запросить состоя- ние команд и их выполнение; ж pdispReserved — интерфейс IDispatch объекта, для которого вызывается меню. Простейшая реализация этого метода может выглядеть следующим образом: function TCustomizedWebBrowser.ShowContextMenu! const dwID: DWORD; const ppt: PPOINT: const pcmdtReserved: IUnknown; const pdispReserved: IDispatch): HRESULT; begi n // Предполагаем, что поле FPopupMenu хранит ссылку 11 на компонент TPopupMenu if Assigned(FPopupMenu) then begin FPopupMenu.Popup!ppt.X. ppt.Y): Result := S_OK: end else Result : = S_FALSE; end: Для полного запрета контекстного меню метод должен всегда возвращать значение S_OK. function GetHostInfo(var plnfo: TDocHostlnfo): HRESULT; stdcall: Приложение может заполнить структуру plnfo, определенную как: TDocHostlnfo - packed record cbSize: ULONG; dwFlags: DWORD: dwDoubleClick: DWORD: end;
428 Глава 9. Применение СОМ-объектов из состава Windows Параметр dwFlags — битовая маска из следующих флагов: Ж DOCHOSTUIFLAG_DIALOG — запрещает выделение текста в форме; Ж DOCHOSTUIFLAG_DISABLE_HELP_MENU — запрещает контекстное меню; в D0CH0STUIFLAG_N03DB0RDER — подавляет вывод трехмерной рамки вокруг компо- нента; Ж DOCHOSTUIFLAG_SCROLL_NO — отключает полосы прокрутки; Ж DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE — запрещает исполнение сценариев; Ж DOCHOSTUIFLAG_OPENNEWWIN — открывает ссылки в новых окнах; Ж DOCHOSTUIFLAG_FLAT_SCROLLBAR — использует «плоский» стиль воспроизведения полос прокрутки; S DOCHOSTUIFLAG_DIV_BLOCKDEFAULT — при вводе символа возврата каретки в режиме редактирования вместо тега <Р> будет использоваться тег <DIV>; М DOCHOSTUIFLAG_ACTIVATE_CLIENTHIT_ONLY — компонент получает фокус только при щелчке мышью в клиентской области окна, при щелчке в не клиентской об- ласти (например, на полосе прокрутке) компонент фокуса не получает. Параметр dwDoubleClick задает реакцию на двойной щелчок мышью и может принимать одно из следующих значений: М DOCHOSTUIDBLCLK_DEFAULT — выполнять действие по умолчанию; Ж DOCHOSTUIDBLCLK_SHOWPROPERTIES — показывать окно свойств страницы; М DOCHOSTUIDBLCLK_SHOWCODE — показывать HTML-код страницы. Метод должен вернуть значение S OK или код ошибки OLE. Например, чтобы создать окно с плоскими полосами прокрутки и без трех- мерной рамки, необходимо реализовать этот метод следующим образом: function TCustomizedWebBrowser.GetHostInfo( var plnfo: TDocHostlnfo): HRESULT: stdcall: begin with plnfo do dwFlags := dwFlags or D0CH0STUIFLAG_N03DB0RDER or DOCHOSTUIFLAG_FLAT_SCROLLBAR: Result := S_OK; end: Следующий метод позволяет перехватить исполнение команд и обработку «горячих» клавиш и заменить ее своей: function TranslateAccelerator(const IpMsg: TMsg: const pguidCmdGroup: TGUID: nCmdID: DWORD): HRESULT: stdcall; Представленный ниже метод позволяет задать путь в реестре, который ком- понент TWebBrowser будет использовать для хранения настроек: function GetOptionKeyPath(var pchKey: PWideChar; dwReserved: DWORD): HRESULT: stdcall;
Использование Microsoft Internet Explorer в приложениях 429 Это дает возможность, например, сделать используемый в программе компо- нент независимым от текущих настроек Internet Explorer. Путь должен находиться в ключе реестра HKEY_CURRENT_USER. Этот метод должен выделить память под строку функцией CoTackMemAlloc. Даже в случае ошибки параметр pchKey должен быть инициализирован значе- нием ni 1 или адресом строки. Метод возвращает S_OK в случае успеха или S_FALSE в противном случае. Типичная реализация этого метода может выглядеть следующим образом: function TCustomi zedWebBrowser.GetOpti onKeyPath( var pchKey: PWideChar; dwReserved: DWORD): HRESULT; var ResultLen: Integer; begin Result := S_FALSE; // В поле TCustomizedWebBrowser.FOptionKeyPath: String 11 хранится путь к настройкам if Length(FOptionKeyPath) > 0 then begin // Получаем длину строки UNICODE ResultLen := MultiByteToWideChar(CP_ACP. 0. PChar(FOptionKeyPath). -1. nil. 0): // Выделяем память под буфер pchKey := CoTaskMemAlloc(ResultLen * SizeOfCWideChar)); // Если выделение успешно - копируем строку в буфер if Assigned(pchKey) then begin MultiByteToWideChar(CP_ACP. 0. PChar(FOptionKeyPath). -1. pchKey. ResultLen): Result := S_OK; end: end else begin // Свойство не задано - инициализируем параметр в nil pchKey := nil; end: end; Существует ряд настроек, которые, несмотря на наличие обработчика GetOpti onKeyPath, в любом случае берутся из стандартных параметров Internet Explorer. Наиболее важными из них являются колонтитулы, используемые при печати. В ранних версиях Internet Explorer (до версии 5.0 включительно) един- ственным способом изменить колонтитулы (или подавить их вывод) является запись перед печатью новых значений в ключ реестра: HKCU\Softwaге\Microsoft\Internet Explorer\PageSetup После окончания печати требуется восстановление прежних значений.
430 Глава 9. Применение СОМ-объектов из состава Windows Следующий метод позволяет вернуть указатель на реализованный в прило- жении интерфейс IDispatch, который будет доступен для сценариев в TWebBrowser: function GetExternal(var ppDispatch: IDispatch): HRESULT; stdcall: Если интерфейс IDispatch не реализуется, то параметр ppDispatch должен быть инициализирован значением ni 1. Метод возвращает S DK в случае успеха или код ошибки OLE в случае неудачи. Методы этого интерфейса доступны из сцена- риев, выполняющихся в TWebBrowser, следующим образом: window.external.MethodName Реализовать интерфейс IDispatch можно, например, при помощи класса TAutoObject. Следующий метод позволяет изменить URL-адрес, по которому осуществля- ется загрузка страницы: function TranslateURL(dwTranslate: DWORD: pchURLIn; PWideChar; var ppchURLOut: PWideChar): HRESULT: stdcall: Параметр pchURLIn указывает на строку, содержащую исходный URL-адрес. Если приложение осуществляет трансляцию, оно должно выделить память под новое значение, используя функцию CoTaskMemAlloc, заполнить буфер новым зна- чением URL и вернуть S_OK. В противном случае необходимо присвоить параметру ppchURLOut значение nil и вернуть S_FALSE. В случае возникновения ошибки метод должен вернуть OLE-код ошибки. Обработчик вызывается только при интерактивном переходе по ссылке из TWebBrowser и не вызывается при переходе при помощи метода Navigate. Доступ к документной модели TWebBrowser В Internet Explorer реализовано расширение HTML под названием Dynamic HTML (DHTML). Эта модель представляет все элементы HTML-документа в виде набора коллекций объектов, доступных для изменения. Сценарии, встроенные в страницы и приложения и имеющие доступ к этим коллекциям, могут находить и изменять их элементы, добавлять новые, причем изменения будут немедленно отражены в окне TWebBrowser. Иерархическое объектное представление HTML-объектов назы- вается DOM (Document Object Model — документная модель объектов). Модель DOM в элементе управления ActiveX, каковым является Internet Explorer, доступна программисту в виде набора COM-интерфейсов. Отправной точкой для доступа к ней служит свойство property Document: IDispatch; Это свойство обеспечивает доступ к интерфейсу IHtmlDocument2, позволяющему работать с содержимым документа. Для получения интерфейса необходимо за- просить его при помощи оператора as:
Использование Microsoft Internet Explorer в приложениях 431 var Document: IHtmlDocument2; Document := WebBrowser.Document as IHtmlDocument2: Документ в DOM представляет собой набор коллекций элементов. Для доступа к коллекции служит интерфейс IHtml Elementcollection, а для доступа к элементу коллекции — IHtmlElement. Следующий фрагмент кода выводит все теги, имею- щиеся в текущем документе, и текст внутри тегов: procedure TForml.ButtonlClick(Sender: TObject): var HtmlDocument: IHtmlDocument2: HtmlCol lection: IHtmlElementCollection: HtmlElement: IHtmlElement: I: Integer: begin Memol.Lines.Cl ear; HtmlDocument := WebBrowser.Document as IHtmlDocument2: HtmlCol 1ecti on := HtmlDocument.All; for I := 0 to HtmlCol lection.Length - 1 do begin HtmlElement := HtmlCollection.Itemd. 0) as IHtmlElement: Memol.Lines.Add(HtmlElement.TagName + ' ' + HtmlElement.InnerText): end: Возможно динамическое создание документов в памяти без необходимости записи их на диск и вызова метода Navigate с протоколом file://. Проиллюст- рируем работу с документной моделью TWebBrowser на примере. Расположим на форме компоненты TWebBrowser, TMemo и три компонента TButton и создадим сле- дующие обработчики событий: uses MSHTML. ActiveX; procedure TForml.FormCreate(Sender: TObject): begin // Инициализируем пустой документ в WebBrowser *WebBrowserl.Navigate('about:blank'): end: procedure TForml.ButtonlClick(Sender: TObject): var Document: IHTMLDocument2: V: OleVariant; begin // Этот метод переписывает в WebBrowser // HTML-документ из TMemo Document := WebBrowserl.Document as IHtmlDocument2:
432 Глава 9. Применение СОМ-объектов из состава Windows V := VarArrayCreate([O. 0]. varVariant): V[0] := Memol.Text; Document.Write(PSafeArray(TVarData(v).VArray)); Document.Close: end; procedure TForml.Button3Click(Sender: TObject): var Document: IHTMLDocument2; Col 1ecti on: IHTMLE1ementCol1ection: Element: IHTMLElement: I: Integer: begin // Этот метод модифицирует текст документа И при помощи DHTML Document := WebBrowserl.Document as IHtmlDocument2; Col 1ect i on := Document.a11: Collection := Col 1ection.Tags('BODY') as IHTMLE1ementCollection: Element := Collection.Item(NULL. 0) as IHTMLElement: Element.InnerText := 'Modifyed by DHTML'; end: procedure TForml.Button2Click(Sender: TObject): var Document: IHTMLDocument2: begin // Этот метод позволяет просмотреть в ТМето код И HTML-документа из WebBrowser Document := WebBrowserl.Document as IHtmlDocument2; Memol.Text := (Document.al 1.Item(NULL. 0) as IHTMLElement).OuterHTML; end; В дизайнере форм поместим в объект Memol.Lines следующий текст: <HTML> <HEAD> <TITLE>Hello World</TITLE> </HEAD> <BODY> Hello again! </BODY> </HTML> Таким образом, мы получили возможность динамически создавать HTML-до- кументы и предоставлять их пользователю. Разумеется, заполнение компонента TWebBrowser строками из многострочного поля лишь пример, однако владение подобной техникой позволит вам динамически формировать и показывать HTML-
Использование Microsoft Internet Explorer в приложениях 433 отчеты. Благодаря простоте языка HTML формирование отчетов является отно- сительно несложной задачей. Для примера рассмотрим код функции, дающей HTML-представление содержимого объекта TDBGrid: function DBGridToHTML(Grid: TDBGrid): String; const // Описываем заголовок HTML и таблицу стилей HTMLStart = •<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 ’ + 'Transitional//EN">'#13 + '<HTML>'#13 + ’<HEAD><META http-equiv=Content-Type content="text/html: ' + 'charset=windows-1251">'#13 + '<STYLE>'#13 + BODY {’#13 + ’ BACKGROUND: white:'#13 + ' COLOR: black:'#13 + ' FONT-FAMILY: arial:'#13 + ' FONT-SIZE: 8pt:'#13 + ' VERTICAL-ALIGN: top’#13 + '}'#13 + 'TABLE {'#13 + ' BACKGROUND: white:'#13 + ' BORDER-BOTTOM: silver Opx solid:'#13 + ' BORDER-LEFT: silver lpx solid;'#13 + ' BORDER-RIGHT: silver Opx solid:’#13 + ' BORDER-TOP: silver lpx solid;'#13 + ' FONT-FAMILY: arial:'#13 + ' FONT-SIZE: 8pt:'#13 + ' FONT-WEIGHT: normal:'#13 + '}'#13 + ’TD {'#13 + ' BORDER-BOTTOM: silver lpx solid:'#13 + ' BORDER-LEFT: silver Opx solid;'#13 + ' BORDER-RIGHT: silver lpx solid:'#13 + ' BORDER-TOP: silver Opx solid;'#13 + ' VERTICAL-ALIGN: top:'#13 + ' TEXT-ALIGN: left:’#13 + '}’#13 + 'TH {'#13 + ’ BACKGROUND: silver:'#13 + ’ BORDER-BOTTOM: gray lpx solid;'#13 + ’ BORDER-LEFT: gray Opx solid;'#13 + ' BORDER-RIGHT: gray lpx solid:’#13 + ' BORDER-TOP: gray Opx solid;’#13 + ’ FONT-WEIGHT: bold;’#13 + ' TEXT-ALIGN: left:’#13 + ’}’#13 + ’.gridr {’#13 +
434 Глава 9. Применение СОМ-объектов из состава Windows ' TEXT-ALIGN: right;’#13 + •}'#13 + '.gride {'#13 + ' TEXT-ALIGN: center:'#13 + •}'#13 + '</STYLE>'#13 + ’<Т1Т1Е>Печать таблицы</Т1ТЬЕ>'#13 + '</HEAD>'#13 + '<B0DY>'#13: HTMLEnd = '</BODY></HTML>'; TableStart = '<TABLE WIDTH="100r CELLSPACING=O CELLPADDING=1>'#13: TableEnd = '</TABLE>'#13: HeaderRowStart = '<TR>'#13: HeaderRowEnd = '</TR>'#13: Headerstart = ’<THEAD>‘#13; HeaderEnd = '</THEAD>'#13; BodyRowStart = '<TR>’#13: BodyRowEnd = '</TR>'#13: BodyStart = '<TB0DY>'#13: BodyEnd = '</TB0DY>'#13: const StyleNames: array [TAlignment] of String = (". 'gridr'. 'gride'); Следующая функция формирует фрагмент HTML-кода для одной ячейки таблицы: function TD(Column: TColumn: IsTitle: Boolean; Widht: Integer): String; var S: String; Align: TAlignment: Tag: String: ClassTag: String; begin S := ": ClassTag := ": if IsTitle then begin // Это строка заголовка Tag := 'TH'; Align := Column.Title.Alignment: ClassTag := StyleNames[Align]; end else begin Tag := 'TD'; Align := Column.Alignment: if Align = taLeftJustify then begin // Эти типы данных всегда 11 выравниваем по правому краю
Использование Microsoft Internet Explorer в приложениях 435 if (Column.Field is TBCDField) or (Column.Field is TCurrencyField) then Align := taRightJustify: // Эти типы данных всегда выравниваем по центру if (Column.Field is TBooleanField) then Align := taCenter; end: ClassTag := StyleNames[Align]: if (Column.Field is TBCDField) or //He подгоняем ширину (Column.Field is TIntegerField) or // столбца для этих (Column.Field is TDateTimeField) then // типов данных S := S + ’ NOWRAP' end: if Length(ClassTag) > 0 then ClassTag := ' class=' + ClassTag; if Widht > 0 then S := S + Formate WIDTH='7d^"'. [Widht]); Result := ’<’ + Tag + ClassTag + S + '>': if IsTitle then begin S := Column.Title.Caption end else begin if Column.Field is TBooleanField then with TBooleanField(Column.Field) do begin if Length(DisplayValues) = 0 then begin if AsBoolean then S := 'да' el se S := 'нет': end else S := Column.Field.DisplayText: end else S := Column.Field.DisplayText: end; if Length(Trim(S)) = 0 then S := '&nbsp': Result := Result + S + '</' + Tag + ‘>'#13: end: Далее формируем HTML-представление таблицы: var BM : String: I : Integer: Widhts: array of Integer: TotalWidht: Integer:
436 Глава 9. Применение СОМ-объектов из состава Windows begin Result : with Grid do begin if Assigned(DataSource) and Assigned(DataSource.DataSet) and DataSource.DataSet.Active then with DataSource.DataSet do begin DisableControls: BM := BookMark: // Сохраняем положение Получаем в массиве Widhts процентное соотношение значений ширины ко- лонок: SetLength(Widhts. Columns.Count): TotalWidht := 0: for I := 0 to Pred(Columns.Count) do begin if Assigned(Columns[I],Field) then begin Widhts[I] := Columns[I].Width: Inc(TotalWidht, Widhts[I]); end: end; for I := 0 to High(Widhts) do begin Widhts[I] := Widhts[I] * 100 div TotalWidht: end: Начинаем формирование таблицы: Result := HTMLStart + TableStart + HeaderStart + HeaderRowStart: Вставляем строку с заголовками: for I := 0 to PredCColumns.Count) do begin if Assigned(Columns.Items[I],Field) then begin Result := Result + TD(Columns.Items[I], True. WidhtsEU): end; end: Result := Result + HeaderRowEnd + HeaderEnd: Проходим по источнику данных: First; Result := Result + BodyStart; while not Eof do begin Result := Result + BodyRowStart: for I := 0 to Pred( Columns.Count ) do begin if Assigned(Columns.Items[I].Field) then begin Result := Result + TD(Columns.Items[I], False, -1): end; end;
Автозавершение при вводе данных 437 Result := Result + BodyRowEnd; Next: end: Добавляем завершающий HTML-код и восстанавливаем положение указателя текущей записи в наборе данных: Result := Result + BodyEnd + TableEnd + HTMLEnd: BookMark : = BM: EnableControls: end: end: end; Вставив полученную из этой функции строку в TWebBrowser, можно легко экс- портировать или распечатать данные. Автозавершение при вводе данных Всем работавшим с Internet Explorer знакома функция автозавершения, облег- чающая ввод повторяющихся данных. Работа с таким полем ввода гораздо удоб- нее, чем с простым компонентом TEdi t (рис. 9.5). i Неэед J&apecj * “* ' Поиск '«ТИэГранноа www.mic H т^Перех http//www.microsoft.com/rus/msdn http://www.microsoft.com/rus/iTisdn/sql http://www.microsoft.com/rus/msdn/sqlsi http://www.microsoft.com/rus/sql http://www.microsoft.com/windows/ie_inj . & Рис. 9.5. Работа в Internet Explorer с функцией автозавершения В этом разделе мы создадим компонент, обладающий аналогичной функцио- нальностью. Механизм работы За функциональность автозавершения отвечает COM-объект AutoComplete. Он реа- лизует интерфейс IAutoComplete2, объявленный следующим образом: lAutoComplete = interface!IUnknown) ['{00bb2762-6а77-1ldO-a535-00c04fd7d062}'] function Init(hwndEdit: HWND: punkACL: IUnknown; pwszRegKeyPath: PWideChar; pwszQuickComplete: PWideChar): HRESULT: stdcall: function EnabletfEnable: BOOL): HRESULT; stdcall: end:
438 Глава 9. Применение СОМ-объектов из состава Windows lAutoComplete2 = interface(IAutoComplete) ['{EAC04BC0-3791-1Id2-BB95-0 060977B464C}'] function SetOptions(dwFlag: DWORD): HRESULT: stdcall: function GetOptionstvar dwFlag: DWORD): HRESULT: stdcall: end: Рассмотрим подробнее методы объекта и их параметры: function Init(hwndEdit: HWND: punkACL: IUnknown; pwszRegKeyPath: PWideChar: pwszQuickComplete: PWideChar): HRESULT; stdcall; Эта функция является ключевой и служит для инициализации объекта и под- ключения его к полю ввода. Ниже перечислены параметры функции. hwndEdi t: HWND — дескриптор окна поля ввода, для которого необходимо орга- низовать автозавершение. После инициализации и до тех пор, пока существует окно, объект остается «прикрепленным» к полю ввода, даже если приложение «отпустило» его интерфейс. ж punkACL: IUnknown — ссылка на интерфейс IUnknown объекта, предоставляющего список введенных ранее строк. Этот объект мы обсудим ниже (см. раздел «По- лучение списка истории»). pwszRegKeyPath: PWideChar — путь в реестре к строковому ключу, содержащему строку для форматирования результата. Ключ ищется сначала в разделе HKEY_ CURRENTJJSER, затем в разделе HKEY_LOCAL_MACHINE. Если соответствующий ключ найден, его значение используется для форматирования строки автозаверше- ния. В качестве этого параметра может быть передано значение nil. В этом случае поиск в реестре не производится. И pwszQuickComplete: PWideChar — строка с шаблоном для форматирования резуль- тата. Этот способ форматирования является альтернативой заданию ключа в реестре. Если форматирование результата не требуется, в качестве параметра можно передать значение nil. Форматирование результата автозавершения используется в основном для ввода URL и позволяет по нажатию пользователем клавиш Ctrl+Enter выполнить подстановку введенной им строки в шаблон. Форматирование осуществляется функцией sprintf, в которую передаются шаблон и строка. Например, если в качестве шаблона задано значение http://www.^s.com и поль- зователь ввел слово borland, а потом нажал клавиши Ctrl+Enter, то в поле ввода будет подставлена строка: http://№/м.bo г1 and.com Следующая функция разрешает или запрещает автозавершение для поля: function Enable(fEnablе: BOOL); HRESULT; stdcall;
Автозавершение при вводе данных 439 Две функции позволяют соответственно установить и получить флаги, регу- лирующие поведение объекта автозавершения: function SetOptions(dwFlag: DWORD): HRESULT: stdcall: function GetOptions(var dwFlag: DWORD): HRESULT: stdcall: Флаги могут принимать следующие значения: я ACO_AUTOSUGGEST — включает показ списка истории (history list) ввода; ACO_AUTOAPPEND — включает автоматическое дополнение поля ввода подходя- щим значением из списка истории; ACO_SEARCH — добавляет в список истории пункт Поиск, при выборе которого пользователем приложение должно обеспечить поиск введенного в поле вво- да значения; ACO_FILTERPREFIXES — употребляется при вводе URL и позволяет при автома- тической подстановке пропускать часто встречающиеся префиксы, такие как http:// или www://; Ж ACO_USETAB — позволяет по нажатию клавиши Tab перейти не к следующему полю ввода, а в список истории; в ACF_UPDOWNKEYDROPSLIST — позволяет при пустом поле ввода открыть список ис- тории нажатием клавиши Т или i. Получение списка истории При рассмотрении функции lAutoCompl ete2. Init мы пропустили параметр punkACL и вынесли его описание в отдельный раздел. Объект AutoCompl ete обращается к интерфейсу punkACL, чтобы получить список истории ввода. При этом AutoComplete запрашивает у punkACL интерфейс lEnumString: lEnumString = interface(IUnknown) ['{00000101-0000 - 0000-СООО-000000000046}'] function NextCcelt: ULONG; rgelt: PPWideChar: pceltFetched: PLongWord): HRESULT: stdcall: function Skip(celt: LongWord): HResult; stdcall: function Reset: HResult; stdcall: function CloneCout Enum: lEnumString): HResult: stdcall: end: Этот интерфейс позволяет получить от запрашиваемого объекта список строк. При этом для AutoComplete не важно, каким образом реализован объект, возвра- щающий список строк, что позволяет реализовать различные варианты истории ввода. В частности, Windows предоставляет готовые реализации, позволяющие обратиться к истории ввода Internet Explorer, к истории ввода окна запуска про- грамм (открывается командой Start ► Run) и к пространству имен оболочки Windows (в том числе и к файловой системе). В этой главе мы реализуем свой объект, предоставляющий историю ввода из объекта TStrings, а вообще вы можете аналогично реализовать ввод из баз данных или любого другого источника.
440 Глава 9. Применение СОМ-объектов из состава Windows Целевая операционная система В MSDN сказано, что объект AutoComplete доступен, начиная с Windows Me и Win- dows 2000. Однако реально это не совсем так. Объект доступен в любой версии Windows при установленном браузере Internet Explorer 5.0 и выше. Рассматривае- мый в этой главе компонент при отсутствии в системе объекта AutoCompl ete будет работать как обычный компонент TEdit. Реализация компонента lEnumString Первое, что нам понадобится для поддержки автозавершения, — это СОМ-объ- ект, реализующий интерфейс lEnumString и предоставляющий список строк из объекта TStrings Delphi. Для этого давайте рассмотрим подробнее метод lEnumString: function Reset: HResult: stdcall: Метод вызывается, чтобы инициировать новый перебор строк. После вызова этой функции интерфейс lEnumString должен предоставлять строки, начиная с пер- вой. Следующий метод вызывается для получения одной или нескольких строк: function Next(celt: ULONG: rgelt: PPWideChar; pceltFetched: PLongWord): HRESULT: stdcall: В функцию передается запрашиваемое количество строк в параметре celt, указатель на первый элемент массива указателей на строки rgelt и адрес пере- менной peel tFetched типа LONGWORD. Функция должна выделить память под строки путем вызова CoTaskMemAlloc, заполнить массив rgelt указателями на эти строки и, если параметр pceltFetched не равен nil, вернуть количество возвращенных строк в указываемую этим параметром переменную. При вызове следующего метода объект должен пропустить celt строк: function Skiptcelt: LongWord): HResult: stdcall: Вызов метода Cl one заставляет объект клонировать себя: function Clone(out Enum: lEnumString): HResult: stdcall; Клонировать означает создать такой же объект и вернуть указатель на него в параметре Enum. Таким образом, класс Delphi, реализующий требуемую нам функциональность, будет выглядеть следующим образом: lEnumString = classCHnterfacedObject. lEnumString) FEnumPosition: Integer; FHistory: TStrings; {lEnumString} function Nextlcelt: ULONG: rgelt: PPWideChar: pceltFetched: PLongWord): HRESULT: stdcall: function SkipCcelt: LongWord): HResult: stdcall;
Автозавершение при вводе данных 441 function Reset: HResult: stdcall; function Clone(out Enum: lEnumString): HResult; stdcall; {lEnumString} constructor Create(AHistory: TStrings): end: В конструкторе мы просто запоминаем ссылку на список элементов истории: constructor TEnumStri ng.Create(AHI story: TStri ngs): begin inherited Create: FH1story := AHI story: end; Текущая позиция в списке хранится в переменной FEnumPosition, и метод Reset просто сбрасывает ее в 0 и возвращает код успешного завершения. function TEnumString.Reset: HResult: begin FEnumPosition : = 0: Result := S_OK: end; Метод Skip пытается пропустить требуемое количество элементов. При этом он проверяет, имеется ли такое количество в списке истории, и, если их недоста- точно, возвращает ошибку: function TEnumString.Skip(celt: LongWord): HResult; var Total: Integer: begin Result := S_FALSE; Total := FHistory.Count: {$WARNINGS OFF} if (FEnumPosition + celt) <= Total then begin {$WARNINGS ON} Result := S_OK; Inc(FEnumPosition. celt) end; end; Самым сложным является метод Next. Он «проходит» по списку истории и для каждой строки: » распределяет память; Я преобразует строку в формат Unicode; М копирует указатель на строку в соответствующую позицию массива; И увеличивает счетчик возвращенных строк. function TEnumString.Next(celt: ULONG; rgelt: PPWideChar; pceltFetched: PLongWord): HRESULT;
442 Глава 9. Применение СОМ-объектов из состава Windows var I: Cardinal; Len: Integer; S: String; begin Result := S_OK; I := 0; with FH1story do begin while (FEnumPosition < Count) and (I < celt) do begin S := FHistory[FEnumPosition]: Len := MultiByteToWideChar(CP_ACP, 0, PChar(S), -1. rgeltA. 0) * SizeOf(WideChar); rgelt* := CoTaskMemAlloc(Len): MultiByteToWideChar(CP_ACP. 0. PChar(S). -1, rgelt', Len); Inc(rgelt): Inc(I): Inc(FEnumPosition): end; end; if I <> celt then Result ;= SJALSE: if Assigned(pceltFetched) then peeltFetched'' := I; end; Наконец, метод Cl one создает копию объекта и возвращает ссылку на нее: function TEnumStrIng.Clone(out Enum: lEnumString): HResult; var EnumObject: TEnumString; begin EnumObject := TEnumString.Create(FHistory); EnumObject.FEnumPosition := FEnumPosition; Enum ;= EnumObject as lEnumString; Result := S_OK; end: Спецификации компонента Реализуемый нами компонент будет наследником класса TCustomEdit с некоторыми дополнительными свойствами и методами (они перечислены ниже), которые по- зволят нам управлять автозавершением. Следующее свойство определяет список истории: property AcHistory: TStrings; Ответственность по заполнению и сохранению списка истории лежит на ис- пользующей компонент программе, поскольку невозможно угадать, какая функ- циональность потребуется.
Автозавершение при вводе данных 443 Для облегчения работы со списком добавим в компонент два метода, позво- ляющие соответственно добавить в список истории и удалить из него указанную строку: procedure AcAddToHistory(const S: String): procedure AcRemoveFromHistory(const S: String); Для предотвращения неконтролируемого роста списка создадим свойство, определяющее максимальное количество его элементов: property AcLimit: Integer: При добавлении новых элементов самые старые удаляются. Следующее свойство содержит набор флагов, управляющих режимом работы автозавершения: property AcOptions: TAutoCompleteOptions: Тип TAutoCompleteOptions объявлен так: TAutoCompleteOption = ( acoAutoSuggest, acoAutoAppend. acoSearch. acoFil terPrefixes. acoUseTab. acollpDownKeyDropLi st ): TAutoCompleteOptions = set of TAutoCompleteOption: Назначение элементов понятно из названия и аналогично предназначению флагов метода IAutoComplete2. GetOpti ons. Следующее свойство разрешает или запрещает автозавершение: property AcEnabled: Boolean: Еще одно свойство определяет источник, из которого берется список истории: property AcSource: TAutoCompleteSource: Это свойство может принимать следующие значения: я acsAcHi story — список берется из свойства AsHi story; Я acsShel 1 Namespace — список берется из пространства имен оболочки Windows (например, попробуйте ввести С:\ и посмотрите, что будет в списке исто- рии); acsMRU — список последних запущенных программ в окне запуска программ (открывается командой Start ► Run); acs I EHistory — список берется из журнала Internet Explorer; Ж acsCustom — список должен быть создан в обработчике события OnAcCreate- Enumerator, что позволяет вам создавать собственные реализации lEnumString.
444 Глава 9. Применение СОМ-объектов из состава Windows Следующим представлен обработчик события, возникающего при создании объекта-списка: property OnAcCreateEnumerator; Можно создать свой объект или выполнить инициализацию уже созданного предопределенного объекта. Замечания по реализации Объект AutoComplete привязывается к дескриптору окна с полем ввода. Это создает две проблемы. 8 Объект не уничтожается, пока «живо» окно, независимо от того, есть на него интерфейсные ссылки или нет. Целесообразность подобного поведения вызы- вает большие сомнения, но в Microsoft решили, что надо работать именно так. 8 Библиотека VCL при изменении некоторых параметров объекта TEd 11 создает окно заново. При этом объект AutoComplete теряет свое поле ввода, так что надо и его создать заново. Создание компонента С учетом всего вышесказанного объявление создаваемого компонента будет вы- глядеть следующим образом: type TOnAcCreateEnumerator = procedure ( var Enumerator: lEnumString) of object; TCustomAcEdit = class(TCustomEdit) private FACOptions: DWORD; FAcFailed: Boolean: FAutoCompl ete: IAutoComplete2: FAcEnabled: Boolean; FOnAcCreateEnumerator: TOnAcCreateEnumerator: FAcLimit: Integer; FAcHistory: TStrings; FAcSource: TAutoCompleteSource; procedure SetAcEnabled(const Value: Boolean); function GetAcOptions: TAutoCompleteOptions; procedure SetAcHistory(const Value: TStrings); procedure SetAcLimit(const Value: Integer): procedure SetAcOptions(const Value: TAutoCompleteOpt i ons); procedure CheckAcLimit; procedure InitAutoComplete; procedure DoneAutoComplete: procedure SetAcSource(const Value: TAutoCompleteSource); protected
Автозавершение при вводе данных 445 procedure CreateWnd; override: procedure DestroyWnd: override; function AcCreateEnumerator: lEnumString; virtual: property AcOptions: TAutoCompleteOptions read GetAcOptions write SetAcOptions; property AcLimit; Integer read FAcLimit write SetAcLimit; property AcHistory: TStrings read FAcHistory write SetAcHistory: property AcEnabled: Boolean read FAcEnabled write SetAcEnabled; property AcSource: TAutoCompleteSource read FAcSource write SetAcSource: property OnAcCreateEnumerator: TOnAcCreateEnumerator read OnAcCreateEnumerator write FOnAcCreateEnumerator; public procedure AcAddToHistory(const S: String): procedure AcRemoveFromHistory(const S: String): constructor Create(AOwner: TComponent): override: destructor Destroy: override: end; Рассмотрим реализацию объявленных методов. Для удобства рассмотрения они сгруппированы по выполняемым функциям. Реализация некоторых неклю- чевых методов опущена, но с ней можно ознакомиться в исходных текстах ком- понента, приведенных на прилагаемом компакт-диске. В конструкторе создается объект списка истории и инициализируются началь- ные значения переменных: constructor TCustomAcEdi t.Create(AOwner: TComponent): begin inherited: FAcHistory := TStringList.Create: FAcOptions := ACO_AUTOSUGGEST or ACO_AUTOAPPEND; FAcLimit := 100; FAcEnabled := True: FAcSource := acsAcHistory: end; В деструкторе производится освобождение выделенной памяти и разрушение созданных объектов: destructor TCustomAcEdit.Destroy; begin DoneAutoComplete: FAcHistory.Free; inherited: end:
446 Глава 9. Применение СОМ-объектов из состава Windows Метод CreateWnd вызывается у наследников класса TWinControl при создании окна. По завершении метода мы получаем дескриптор, после чего можно ини- циализировать автозавершение: procedure TCustomAcEdi t.CreateWnd: begin inherited: InitAutoComplete: end: Метод DestroyWnd вызывается при уничтожении окна. Объект AutoComplete для этого окна нам больше не нужен и мы его освобождаем: procedure TCustomAcEdit.DestroyWnd: begin inherited: FAutoComplete := nil; end: В этом методе производится уничтожение объекта AutoCompl ete. Как уже от- мечалось выше, пока существует окно, уничтожить объект нельзя, поэтому мы организуем повторное создание окна. Чтобы избежать мелькания экрана, на время повторного создания окна перерисовка его владельца запрещается. По оконча- нии работы метода владелец и заново созданное окно принудительно перерисо- вываются. procedure TCustomAcEdi t.DoneAutoComplete: begin if Assigned(FAutoComplete) then begin FAutoComplete := nil: if Assigned(Parent) then Parent.Perform(WM_SETREDRAW. 0. 0): try RecreateWnd: finally if Assigned(Parent) then begin Parent.Perform(WM_SETREDRAW. 1, 0); RedrawWindow(Parent.Handle, nil. 0. RDWJNVALIDATE): end: end: end; end: Этот метод инициализирует автозавершение. Поскольку он довольно сложен, мы рассмотрим его подробнее. Инициализация: procedure TCustomAcEdit.InitAutoComplete: var EnumStrings: IUnknown:
Автозавершение при вводе данных 447 hr: HResult: begin Если мы находимся на этапе разработки приложения или загружаем компо- нент — ничего инициализировать не надо: if (csDesigning in Componentstate) or (csLoading in Componentstate) then Exit: Уничтожаем ранее созданный объект AutoCompl ete: DoneAutoComplete; Если при предыдущем создании произошла ошибка (например, не установ- лен браузер Internet Explorer 5) или автозавершение запрещено — выходим: if FAcFailed or not FAcEnabled then Exit: Если окно уже создано: if Handle > 0 then begin пытаемся создать COM-объект. Если создание завершилось неудачно, то уста- навливаем переменную FAcFailed равной True и в дальнейшем не пытаемся его инициализировать: hr := CoCreateInstance(CLSID_AutoComplete. nil. CLSCTX_INPROC_SERVER. IID_AutoComplete, FAutoComplete): if hr = S_OK then begin try Функция AcCreateEnumerator ответственна за создание объекта, предоставляющего список истории. Если список создан — инициализируем объект AutoComplete и уста- навливаем его флаги: EnumStrings := AcCreateEnumerator; if Assigned(EnumStrings) then begin 01 eCheck(FAutoComplete.Ini t(Handl e. EnumStrings. nil, nil)); 01 eCheck(FAutoComplete.SetOpti ons(FAcOptions)): end else DoneAutoComplete: except DoneAutoComplete; FAcFailed := True; end; end else FAcFailed := True; end; end;
448 Глава 9. Применение СОМ-объектов из состава Windows В этой функции создается объект, реализующий интерфейс lEnumString. Если используется предопределенный объект Windows, то создается соответствующий COM-сервер, если используется встроенный список, то создается экземпляр класса TEnumStri ng и ему передается список истории. В заключение, если назначен обработчик OnAcCreateEnumerator, то он вызывается и в него передается создан- ный объект для инициализации: function TCustomAcEdit.AcCreateEnumerator: lEnumString: begin Result := nil; case FAcSource of acsAcHistory: Result := TEnumString.Create!FAcHistory) as lEnumString: acsShellNamespace: Result := CreateComObject(CLSID_ACListISF) as lEnumString: acsMRU: Result := CreateComObject(CLSID_ACLMRU) as lEnumString: acsIEHistory: Result : = CreateComObject(CLSID_ACLHistory) as lEnumString; acsCustom: ; end: if Assigned(FOnAcCreateEnumerator) then OnAcCreateEnumerator(Result); end: Помимо рассмотренной выше функциональности, объект AutoComplete пре- доставляет некоторые дополнительные возможности, связанные с его настрой- кой. Использование интерфейсов lACList и IACList2 Интерфейс I ACL i st предназначен для создания списков автозавершеиия по древо- видным структурам данных. Он имеет всего один метод: lACList = interface(IUnknown) ['{77А130В0-94FD-1IDO-A544-00C04FD7d062}'] function Expand(pszExpand: PWideChar): HRESULT: stdcall: end: Если объект, предоставляющий список строк, реализует интерфейс lACList, то при вводе пользователем в строку символа прямого (/) или обратного (\) слэша вызывается его метод Expand, в который передается текущее содержание строки ввода. После этого содержимое списка истории запрашивается повторно.
Автозавершение при вводе данных 449 Можно при этом предоставить новый список, раскрывающий следующий уровень иерархии. Интерфейс IACL1 st2 имеет два метода для установки и получения флагов ав- тозавершения: IACList2 = interface(IACList) ['{470141а0-5186-1Id2-bbb6-0060977b464c}'] function SetOptions(dwFlag: DWORD): HRESULT: stdcall: function GetOptions(var pdwFlag: DWORD): HRESULT: stdcall: end: Этот интерфейс реализует объект, предоставляющий список строк, соответст- вующих файловой системе. Флаги задают ветвь пространства имен Windows, по которой будет осуществляться навигация. Выбор целевой папки для навигации Если нужно задать конкретную папку, по которой будет осуществляться навига- ция при помощи списка строк, представляющего файловую систему, можно вос- пользоваться реализуемым этим списком интерфейсом IPersistFolder. Сделать это можно в обработчике события AcCreateEnumerator: procedure TForml.AcEditlAcCreateEnumerator( var Enumerator: lEnumString); var Malloc: IMalloc; PIDL: PItemIDList; begi n if AcEditl.AcSource = acsShellNamespace then begin if Assigned(Enumerator) then begin PIDL := nil: 01eCheck(SHGetMalloc(MAlloc)): try DleCheck(SHGetSpecialFolderLocation(Handle. CSIDL_PERSONAL, PIDL)): 01 eCheck((Enumerator as PersistFolder).Initialize(PIDD): finally Malloc.Free(PIDL): Malloc := nil: end: end: end; end: Данный код организует навигацию по папке Мои документы.
450 Глава 9. Применение СОМ-объектов из состава Windows Создание списков истории из нескольких источников Иногда может понадобиться предоставить комбинированную информацию из не- скольких списков (например, из списка файловой системы и собственного спи- ска). В этом случае нужно воспользоваться объектом ACLMulti и реализуемым им интерфейсом lObjMgr. Для этого надо установить значение параметра AcSource равным acsCustom и создать список в обработчике события AcCreateEnumerator: procedure TForml.AcEditlAcCreateEnumerator( var Enumerator: lEnumString): begin Enumerator : = CreateComObject(CLSID_ACLMulti) as lEnumString: (Enumerator as lObjMgr).Append( CreateComObject(CLSID_ACLi st ISE)): (Enumerator as lObjMgr).Append( TEnumString.Create(AcEditl.AcHistory) as lEnumString): end: После этого в списке истории можно увидеть как содержимое пространства имен оболочки Windows, так и пункты из своего списка истории. Тестовая программа Приведенная на компакт-диске тестовая программа позволит вам проверить функ- циональность компонента и влияние различных флагов на его поведение, а также проиллюстрирует основные приемы работы с ним (рис. 9.6). Рис. 9.6. Работа приложения, использующего автозавершение
Добавление вкладок в диалоговое окно свойств файла 451 Таким образом, мы изучили реализацию автозавершения и создали компо- нент, обладающий соответствующей функциональностью. Добавление вкладок в диалоговое окно свойств файла Всем знакомо диалоговое окно свойств, появляющееся, если в Проводнике щелк- нуть на имени файла правой кнопкой мыши и выбрать в контекстном меню команду Свойства. Многие наверно заметили, что для некоторых типов файлов в этом диалоговом окне имеются дополнительные вкладки, позволяющие получить ин- формацию, касающуюся именно этого типа файла. В данном разделе мы обсудим, как создать собственное расширение оболочки Windows, позволяющее предоста- вить аналогичную функциональность для файлов наших приложений. Механизм работы При формировании диалогового окна свойств файла оболочка Windows выпол- няет следующие действия. 1. Ищет в реестре раздел HKEY_CLASSES_ROOT\.ext, где ext — расширение файла. Например, для расширения *.txt этот раздел будет выглядеть так: HKEY_CLASSES_ROOT\.txt 2. Считывает из этого раздела значение, заданное по умолчанию, например, для расширения *.txt в нем задана строка: txtfile 3. Ищет в секции HKEY_CLASSES_ROOT раздел, совпадающий по имени со строкой, полученной в п. 2. 4. В этом разделе проверяет подраздел she!1exXPropertySheetHandl ers 5. Если такой подраздел существует, то из пего считываются значения иденти- фикаторов GUID для COM-серверов (рис. 9.7). Рис. 9.7. Запись в системном реестре ссылки на COM-сервер для создания вкладок
452 Глава 9. Применение СОМ-объектов из состава Windows 6. Оболочка Windows предполагает, что эти серверы реализуют интерфейсы IShellExtlnit, IShellPropSheetExt. Она создает экземпляры СОМ-объектов, за- прашивает у них эти интерфейсы и при помощи их методов позволяет доба- вить в диалоговое окно свойств файла новые страницы. Чтобы понять, как это происходит, рассмотрим подробнее методы используе- мых интерфейсов: IShellExtlnit = interface!IUnknown) [SIDJShellExtlnit] function Initialize(pidlFolder: PItemIDList: Ipdobj: IDataObject: hKeyProglD: HKEY): HResult: stdcall: end: Единственный метод этого интерфейса вызывается оболочкой Windows для инициализации расширений и служит для передачи в него контекста, в котором вызвано расширение (текущая папка, выбранный объект). Параметр pidlFolder для диалогового окна всегда содержит nil, а в параметре Ipdobj передается ссылка на интерфейс IDataObject, при помощи которого можно получить информацию об объекте, для которого вызвано расширение оболочки. IShellPropSheetExt = interface!IUnknown) [SIDJShell PropSheetExt] function AddPages(IpfnAddPage: TFNAddPropSheetPage: IParam: LPARAM): HResult; stdcall: function ReplacePage(uPageID: UINT; 1pfnReplaceWi th: TFNAddPropSheetPage: IParam: LPARAM): HResult: stdcall; end: Второй из используемых интерфейсов характерен только для обработчика диалогового окна свойств и содержит методы, позволяющие добавить страницы в диалоговое окно либо заменить уже имеющиеся там новыми. Нам понадобится только первый метод, поскольку второй используется для страниц Панели управ- ления Windows. В метод AddPages передаются два параметра. М LpfnAddPage — адрес функции, которую наше расширение может вызвать для регистрации своих страниц. Страницы должны быть созданы функцией CreatePropertySheetPage. я LParam — параметр, который мы должны передать в эту функцию. Таким образом, для создания своих вкладок необходимо выполнить следую- щую процедуру. В методе IShellExtlnit. Initialize получить и запомнить имя файла, для кото- рого требуется показать страницы свойств. 1. В методе IS he 11 PropSheetExt. AddPages создать и добавить требуемые страницы. Функция LpfnAddPage имеет следующий тип: LPFNADDPROPSHEETPAGE = function(hpsp: HPropSheetPage:
Добавление вкладок в диалоговое окно свойств файла 453 1 Param: Longint): BOOL stdcall: TFNAddPropSheetPage = LPFNADDPROPSHEETPAGE; Параметр 1 Param нам передают. Параметр HPSP мы должны получить при по- мощи функции: function CreatePropertySheetPage( var PSP: TPropSheetPage): HPropSheetPage: stdcall: Эта функция принимает на входе структуру PSP, объявленную как: TPropSheetPage = record dwSize: Longint: dwFlags: Longint: hlnstance: THandle: case Integer of 0: ( pszTemplate: PWideChar): 1: ( pResource: Pointer: case Integer of 0: ( hlcon: THandle): 1: ( pszlcon: PWideChar: pszTitle: PWideChar; pfnDlgProc: Pointer; 1 Pa ram: Longint; pfnCallback; TFNPSPCallbackW; pcRefParent: PInteger; pszHeaderTitle; PWideChar; 11 это отображается в заголовке pszHeaderSubTitle: PWideChar)); end: От приложения требуется корректно заполнить требуемые поля структуры. Ж dwSize — размер структуры, должен быть установлен в SizeOf(TPropSheetPage). в dwFl ags — набор флагов, определяющих поведение создаваемой страницы. Если не требуется какой-то особой функциональности, можно использовать значе- ние PSP_DEFAULT. » Hlnstance — ссылка на вызывающий модуль. К pszTemplate (pResource) — имя ресурса с описанием диалогового окна. Создание этого ресурса мы рассмотрим ниже. Если используется флаг PSPJJLGINDIRECT, то страница может быть создана из ресурса, уже загруженного в память. При этом используется указатель pResource. ж Hlcon (pszlcon) — если задан флаг PSPJJSEICON, вкладка будет иметь значок, деск- риптор которого задан в параметре Hlcon, если используется флаг PSPJJSEICONID, значок будет загружен из ресурса с именем pszlcon.
454 Глава 9. Применение СОМ-объектов из состава Windows S PszTitle — если задан флаг PSPJJSETITLE, то вкладка будет иметь заголовок, за- данный этим параметром, а не ресурсом диалогового окна. И pfnDlgProc — адрес функции, обрабатывающей сообщения для создаваемой страницы. S 1 Param — произвольное значение После создания страницы Windows пошлет ей сообщение WM INITDIALOG с этим значением в параметре 1 Param. S pfnCallback — адрес функции обратного вызова (callback function), вызывае- мой при создании и уничтожении страницы. Используется, если задан флаг PSPJJSECALLBACK. ж pcRefParent — адрес переменной со счетчиком ссылок на страницу. Использу- ется, если задан флаг PSPJJSEREFPARENT. Как можно заметить, описание страницы берется из ресурса, а за ее поведе- ние отвечает функция pfnDlgProc. Это несколько непривычно для программи- ста Delphi, но именно таким образом осуществляется создание диалоговых окон в Windows API. Впрочем, все не так уж сложно, и программист, знакомый с сооб- щениями Windows и оконной процедурой, легко сможет понять, как создавать свои диалоговые окна. На этом вводную часть можно считать оконченной и можно приступить к реа- лизации. Создание СОМ-сервера Расширение оболочки — это внутрипроцессный COM-сервер, скомпилирован- ный в виде DLL. Чтобы создать его, следует выбрать в меню Delphi команду File ► New ► Other, перейти на страницу ActiveX репозитария объектов и выбрать значок ActiveX Library. Будет создана библиотека. Затем на той же странице нужно выбрать значок COM Object. В появившемся диалоговом окне следует ввести имя создаваемого класса Delphi (например, TTextProp) и снять флажок Include Туре Library. Будет сгенерирован шаблон СОМ-сервера. Теперь добавим в секцию uses модуль ShiObj, в котором объявлены требуемые интерфейсы, и добавим эти интерфейсы в описание класса. Также добавим поле FileName для хранения имени файла, с которым нам придется работать. После этого объявление класса должно выглядеть следующим образом: type TTextProp = class(TComObject. IShel lExt Init. IShellPropSheetExt) FileName : PChar; {IShellExtlnit} function InitializelpidlFolder: PltemlDList: Ipdobj: IDataObject: hKeyProgID: HKEY): HResult: reintroduce: stdcall: {IShe 11PropSheetExt} function AddPagesdpfnAddPage: TFNAddPropSheetPage: 1 Pa ram: LPARAM): HResult: stdcall:
Добавление вкладок в диалоговое окно свойств файла 455 function ReplacePageluPagelD: UINT: 1pfnReplaceWi th: TFNAddPropSheetPage: IParam: LPARAM): HResult; stdcall: end: const Class_TextProp: TGUID = ' {F7195E61-C384-11D2-89E8-80FA4797BEC7}'; Можно приступать к реализации методов. Метод Initialize взят из примера Contmenu.dpr, входящего в комплект поставки Delphi, метод ReplacePage не делает ничего, кроме возвращения кода успешного завершения. Таким образом, наиболее интересен код метода AddPages: function TTextProp.AddPagesI 1pfnAddPage: TFNAddPropSheetPage; IParam: LPARAM): HResult: stdcall: var PSP : TPropSheetPage: hPage : HPROPSHEETPAGE: begin // Добавляем свою страницу свойств Result := E_FAIL: Fi1ICharlPSP. SizeOf(PSP). 0); with PSP do begin dwSize := SizeOf(PSP): dwFlags := PSP_DEFAULT or PSP_USEICONID; hlnstance := Syslnit.hlnstance: pszlcon := 'MAINICON'; pszTemplate := 'SheetDialog': //Имя шаблона в ресурсе pfnDlgProc := @DialogProc: IParam := Integer(Self): // Ссылка на себя для DialogProc end: hPage := CreatePropertySheetPage(PSP); if Assigned(hPage) then begin // OK, создали страницу успешно, // добавляем ее в PageControl IpfnAddPagelhPage. IParam): // Увеличиваем счетчик ссылок _AddRef: Result := NOERROR: end: end; Как видим, сам по себе метод не сложен, но в его коде есть один тонкий мо- мент. Дело в том, что оболочка Windows после вызова этого метода «отпускает» СОМ-сервер, вызывая его метод IUnknown. Release, что приводит к его немедлен-
456 Глава 9. Применение СОМ-объектов из состава Windows ному разрушению и выгрузке DLL из памяти. Однако это явно не входит в наши планы, а поэтому мы должны проделать описанную ниже процедуру. Принудительно увеличить счетчик ссылок вызовом метода _AddRef. 1. По завершении работы с диалоговым окном вызвать метод Release для осво- бождения памяти. 2. Запомнить где-то ссылку на себя и сохранять ее до конца работы (чтобы вы- звать метод -Release впоследствии). Ссылка запоминается в поле IParam структуры TPropSheetPage. В дальнейшем это поле будет передано в диалоговую процедуру, и мы сможем сохранить ссылку. ВНИМАНИЕ ------------------------------------------------------------------- Это редчайший случай прямого вмешательства в механизм автоматического управле- ния подсчетом интерфейсных ссылок. Без веских на то оснований вызывать методы _AddRef и -Release не надо. Создание описания диалогового окна и диалоговой функции Как мы уже ранее упоминали, описание диалогового окна должно храниться в файле ресурсов программы. Для этого необходимо создать ресурс типа DIALOGEX (файл sheet.ro) и подключить его к проекту. Кроме того, нам понадобится модуль с описаниями констант следующего содержания: unit Constant; interface const ID_MEMO = 1: ID_LOAD = 2: implementation end. В самом файле ресурсов надо добавить следующее описание диалогового окна: #include "constant.pas" SheetDialog DIALOGEX 0. 0. 0. 0 FONT 8. "MS Shell Dig" STYLE DS-SETFONT | DS_FIXEDSYS CAPTION "Delphi Extension" BEGIN EDITTEXT ID_MEMO. 10. 10. 150. 120. ES_MULTILINE | ES_READONLY PUSHBUTTON "SSample". ID_LOAD. 100. 140. 60. 12 END Как видите, описание диалогового окна достаточно простое и понятное. Диало- говое окно будет содержать многострочное поле и кнопку. Более подробно о языке описания ресурсов можно узнать из документации Windows SDK. Однако это лишь описание расположения элементов управления на форме. От нас требуется
Добавление вкладок в диалоговое окно свойств файла 457 написать код, обеспечивающий функциональность диалогового окна. Этот код расположен в функции DialogProc, которая вызывается каждый раз, когда нашему окну приходит сообщение. К счастью, нет необходимости обрабатывать все со- общения Windows (для этого существует функция DefWindowProc), необходимо лишь описать специфическую функциональность, нужную нам. const WM_CREATED = WM_APP + 1: function DialogProc(hWnd: THandle: Msg: UINT: wParam: WPARAM: 1 Param: LPARAM): BOOL: stdcall; var Buffer: array[0..1024] of Char: Count: Integer; hChild: THandle: TP: TTextProp: R: TRect: begi n case Msg of Сообщение WM_INITDIALOG приходит при создании страницы. В поле 1 Param хра- нится значение, которое передается в TPropSheetPag. 1 Param. Для сохранения ссылки на объект TTextProp воспользуемся возможностью поставить в соответствие любому окну произвольное число. Его можно записать вызовом функции SetWindowLong с параметром DWLJJSER, а получить — вызовом функции GetWindowLong. Кроме того, с помощью функции PostMessage мы отправляем сами себе сообщение WM_CREATED. Это гарантирует, что оно придет после полного завершения инициализации окна. WM_INITDIALOG: begin TP := TTextProp(PPropSheetPage(lParam).lParam): SetWindowLong(hWnd. DWL_USER, Integer(TP)): PostMessagelhWnd. WM_CREATED, 0. 0): end: Окно полностью проинициализировано, корректируем координаты элементов управления, чтобы они располагались корректно вне зависимости от разрешения и установленного шрифта: WM_CREATED: begin GetWindowRectlhWnd. R): hChild := GetDlgltemlhWnd. ID_MEMO): MoveWindow(hChild, 20. 20. R.Right - R.Left - 40, R.Bottom - R.Top - 70, FALSE): hChild := GetDlgItem(hWnd. ID_LOAD): MoveWindow(hChild. R.Right - R.Left - 20 - 100. R.Bottom - R.Top - 40. 100. 30. FALSE): end;
458 Глава 9. Применение СОМ-объектов из состава Windows Сообщение WM_COMMAND приходит каждый раз, когда пользователь выбирает в окне какой-то элемент управления. Мы обрабатываем только одну команду — по щелчку на кнопке загружаем в многострочное поле первый килобайт файла: WM_COMMAND: begin if (LoWord(wParam) = ID_LOAD) and (HiWord(wParam) = BN_CLICKED) then begin // Щелкнули на кнопке Sample TP := TTextProp(GetWindowLong(hWnd, DWLJJSER)): with TFileStream.Create(TP.FileName. fmOpenRead) do try Fill Char(Buffer, SizeOf(Buffer), 0): Count : = Size: if Count >= SizeOf(Buffer) then Count := SizeOf(Buffer) - 1: ReadBuffer(Buffer. Count): hChild := GetDlgItem(hWnd. ID_MEMO); SendMessage(hChild, WM_SETTEXT. 0, Integer(@Buffer)): finally Free: end: end: end: Сообщение WM_DESTROY приходит при уничтожении окна. Освобождаем ранее выделенную память и позволяем выгрузиться нашему СОМ-серверу: WM_DESTROY: begin TP := TTextProp(GetWindowLong(hWnd. DWLJJSER)): CoTaskMemFree(TP.FileName): TP._Release: end: Все остальные сообщения передаются в процедуру обработки, предоставлен- ную Windows API: el se Result := BOOK Def WindowProc (hWnd. Msg. wParam. IParam)): Exit: end: Result := True: end: Как видите, создание диалогового окна средствами API — не такая уж слож- ная задача. Регистрация расширения оболочки Наше приложение, которое является COM-сервером, должно прописать в реестре дополнительную информацию о том, к какому расширению файла оно относится.
Добавление вкладок в диалоговое окно свойств файла 459 Удобно делать это одновременно с регистрацией СОМ-сервера. Как известно, реги- страция осуществляется вызовами функций DllRegisterServer и DllUnregisterServer, которые Delphi предоставляет автоматически. Однако мы можем вмешаться в этот процесс и дополнить их функциональность. Для этого откроем файл проекта и мо- дифицируем его. Вначале определим константы, чтобы полученный код было легко модифицировать: const EXT = '.txt'; DESCRIPTIVE = 'txtfile': FRIENDLY_NAME = 'Текстовый документ': KEY_NAME = DESCRIPTIVE + '\shel1ex\PropertySheetHandl ers'; В функции DllRegisterServer после успешной регистрации сервера функцией из модуля ComServ проверим наличие в реестре требуемых ключей и при необхо- димости добавим их: function DllRegisterServer: HResult; stdcall: var RegKey: HKEY: begi n Result := ComServ.DllRegisterServer: if Result = S_OK then begin CreateRegKey(EXT. ". DESCRIPTIVE. HKEY_CLASSES_ROOT): if RegOpenKey(HKEY_CLASSES_ROOT. DESCRIPTIVE. RegKey) <> ERROR_SUCCESS then begin RegCloseKey(RegKey); CreateRegKey(DESCRIPTIVE. ". ". HKEY_CLASSES_ROOT): end: CreateRegKey(Format(KEY_NAME + 'Us'. [GUIDToString(Class_TextProp)]). ", ", HKEY_CLASSES_ROOT); end; end: В функции DllUnregisterServer удалим ненужный ключ с регистрацией рас- ширения оболочки: function DllUnregisterServer: HResult: stdcall: begin Del eteRegKey(KEYNAME. HKEY_CLASSES_ROOT): Result := ComServ.DllUnRegisterServer: end: Осталось зарегистрировать расширение в Windows. На компьютере разработ- чика для этого можно воспользоваться командой Run ► Register ActiveX Server, на компьютере клиента — утилитой RegSvr32 из состава Windows или TRegSvr из
460 Глава 9. Применение СОМ-объектов из состава Windows комплекта поставки Delphi. После регистрации программы выберем в Провод- нике Windows файл с расширением *.txt и посмотрим его свойства. Диалоговое окно свойств показано на рис. 9.8. Рис. 9.8. Демонстрация содержимого ТХТ-файла на специальной вкладке в окне свойств Полные исходные тексты расширения оболочки приведены на прилагаемом компакт-диске. Заключение В настоящей главе мы рассмотрели некоторые примеры использования СОМ- объектов, входящих в состав Windows. В частности, мы обсудили: Ж создание ярлыков (shotcuts) — файлов с расширением ‘.Ink с помощью интер- фейса API оболочки (shell) Microsoft Windows; Я получение уведомлений от Windows Explorer, позволяющих анализировать и обрабатывать сообщения о манипуляциях с каталогами в Windows Explorer с помощью интерфейса ICopyHook; И создание собственных окон просмотра нефайловых данных в Windows Explorer с помощью интерфейсов IShel 1 Folder и IShel 1 View; И OLE-реализацию метода перетаскивания (drag-and-drop) с помощью интер- фейсов IDropTarget, IDropSource, IDataObject и lenumFormatEtc;
Заключение 461 И использование Microsoft Internet Explorer в приложениях и применение для этой цели компонента TWebBrowser, в частности отображение в нем HTML- страниц, печать содержимого, копирование данных в буфер обмена при по- мощи интерфейса lOleCommandTarget объекта WebBrowser.Document, управление поведением TWebBrowser с помощью интерфейса IDocHostUIHandler, обращение к модели DOM, динамическое создание и отображение HTML-документов; й использование автозавершения при вводе данных с помощью интерфейсов lAutoComplete2, lEnumString, lACList, IACL1 st2, lObjMgr; Я добавление вкладок в диалоговое окно свойств файла с помощью интерфей- сов IShellExtlnit и IshellPropSheetExt и их регистрацию. Отметим, что, хотя мы и выбрали примеры, на наш взгляд, наиболее часто встречающиеся на практике, этими примерами возможности применения СОМ- объектов, входящих в состав Windows, далеко не исчерпываются. Поэтому мы продолжим обсуждение вопросов применения объектов, входящих в состав Win- dows, в следующей главе, посвященной использованию интерпретатора Microsoft Script Control.
ГЛАВА 10 Microsoft Script Control При разработке настраиваемых информационных систем часто возникает необхо- димость снабдить свою программу встроенным языком программирования. Такой язык позволял бы конечным пользователям настраивать режим работы программы без участия автора и без перекомпиляции. Однако самостоятельная реализация интерпретатора подобного встроенного языка является непосильной для многих разработчиков задачей, а от большинства остальных потребует очень много вре- мени и усилий. В то же время в Windows, как правило, уже имеется достаточно качественный интерпретатор, который может быть легко встроен в вашу программу. Речь идет о Microsoft Script Control. Он устанавливается вместе с Microsoft Internet Explorer, входит в комплект поставки Windows 2000 и Windows 98, а для предыдущих версий Windows доступен в виде свободно распространяемого отдельного дист- рибутива, объем которого составляет около 200 Кбайт. Получить его можно по адресу http://msdn.microsoft.com/scripting. В дистрибутив входят элемент управ- ления ActiveX и справочный файл с описанием его свойств и методов. Добавление компонента TScriptControl в программу Чтобы добавить интерпретатор Microsoft Script Control на палитру компонентов Delphi (в виде компонента TScriptControl), необходимо импортировать соответст- вующий элемент управления ActiveX (рис. 10.1). После этого на странице ActiveX палитры компонентов появится невизуаль- ный компонент TScriptControl, который можно разместить на форме. Рассмотрим ключевые свойства и методы компонента TScriptControl. Свойство Language задает язык, интерпретатор которого будет реализовывать компонент: property Language: String В стандартной поставке доступны языки VBScript и JScript, однако если в вашей системе установлены расширения Windows Scripting, возможно использование других языков, таких как Perl или Rexx. Свойство Timeout задает временной интервал (timeout) исполнения сценария, по истечении которого генерируется ошибка: property Timeout: Integer
Добавление компонента TScriptControl в программу 463 Рис. 10.1. Импорт интерпретатора Microsoft Script Control Значение -1 позволяет отключить генерацию ошибок, связанных с истечением отведенного временного интервала. В этом случае сценарий может исполняться неограниченное время. При установке свойства UseSafeSubset равным True компонент может выпол- нять ограниченный набор действий, заданный текущими установками безопас- ности в системе: property UseSafeSubset: Boolean Это свойство полезно, если вы запускаете сценарии, полученные, например, через Интернет. Метод AddCode добавляет к списку процедур компонента код, заданный пара- метром Code: procedure AddCode(const Code: WideString): В дальнейшем эти процедуры могут быть вызваны либо при помощи метода Run, либо из других процедур сценария: ScriptControll.AddCode(Memol.Text):
464 Глава 10. Microsoft Script control Метод Eval выполняет код, заданный в параметре Expression, и возвращает ре- зультат исполнения: function Eval(const Expression: WideString): OleVariant Этот метод позволяет выполнить код без добавления его к списку процедур компонента. Метод AddObject добавляет объект к пространству имен компонента: procedure AddObject(const Name: WideString: Object_: IDispatch: AddMembers: WordBool): Добавленный объект должен быть сервером автоматизации. Кроме того, в коде сценария он должен быть доступен как объект. Например, пусть в программе создан сервер автоматизации External, реализующий метод DoSomething(Value: Integer), и пусть мы добавили этот объект к пространству имен компонента: SeriptControll.AddObject('External', TExternal as IDispatch, False); В этом случае в коде сценария мы можем использовать добавленный объект следующим образом: Dim I 1 = 8 + External .DoSomething(8) Метод Run выполняет именованную процедуру из числа ранее добавленных при помощи метода AddCode: function Run(const ProcedureName: WideString; var Parameters: PSafeArray): OleVariant; В массиве Parameters могут быть переданы параметры. Метод Reset сбрасывает компонент в начальное состояние, удаляя все добав- ленные ранее объекты и код: procedure Reset: Таким образом, компонент TScrl ptControl представляет собой достаточно гиб- кую исполняющую систему с возможностями расширения путем добавления в ее пространство имен серверов автоматизации. Интеграция компонента TScriptControl с VCL В существующем виде возможности компонента TScriptControl в значительной степени ограничены из-за сложности доступа к классам VCL. Исполнение ин- терпретируемого кода — это хорошо, однако хотелось бы иметь возможность обращаться из этого кода к компонентам в программе, получать и устанавли- вать их свойства, обрабатывать возникающие в них события, например, следую- щим образом:
Интеграция компонента TScriptControl с VCL 465 Sub Main() Dim Control Control = Self.Controls("Panel2") Control.Add "Panel3”. "TPanel" With Panels .Align = "alTop” .Bevel Outer = "bvNone" .Height = 40 .Caption = "" .Add “Btn”. "TButton”. True With Btn .Top = 10 .Left = .Top .Caption = "Click me" End With End With End Sub Sub Btn_0nClick () Dim StatusBar Dim Panel Dim I I = 0 For Each Panel in StatusBar.Panels 1 = 1 + 1 with Panel .Text = .Text & " " & CStr(I) End With Next End Sub Дальнейшая часть главы посвящена реализации такой функциональности, однако прежде чем приступить к написанию кода, необходимо подробно рассмот- реть некоторые механизмы, лежащие в основе модели расширения компонента TScriptControl и VCL. Модель расширения компонента TScriptControl Как уже было показано выше, интерпретатор Microsoft Script Control позволяет сделать доступными из сценария объекты, реализованные в программе при помощи метода AddObject. При обращении к таким объектам интерпретатор предполагает, что они реализуют интерфейс I Di spatch и являются, таким образом, серверами ав- томатизации. В Delphi в качестве таких объектов могут выступать наследники класса TAutoObject, создать которые можно при помощи мастера, вызываемого выбором значка Automation Object на странице ActiveX репозитария (открывается командой File ► New ► Other). При вызове методов этих объектов интерпретатор Microsoft Script Control последовательно вызывает методы GetldsOfNames и Invoke
466 Глава 10. Microsoft Script Control их интерфейса IDispatch, что обеспечивает вызовы соответствующих методов объекта. Однако здесь имеются определенные сложности. По окончании работы с объектом (например, при выходе его за пределы об- ласти видимости процедуры сценария) компонент TScriptControl автоматически вызывает его метод _Release, что приводит к уничтожению класса Delphi. Таким образом, для каждого класса приходится создавать некий объект-представитель, призванный транслировать вызовы компонента TScriptControl в методы и свой- ства класса Delphi. Став ненужным, этот объект-представитель должен уничто- жаться, причем без уничтожения самого класса. Функциональность наследников класса TAutoObject задается на этапе компиля- ции и не может быть расширена в процессе исполнения программы. Это требует создания отдельных представителей для каждого класса VCL, что очень сложно осуществить, к тому же при этом нельзя использовать классы, не имеющие со- ответствующего представителя. Чтобы найти обходные пути для решения этой проблемы, необходимо более детально вникнуть в реализацию базового интерфейса, лежащего в основе авто- матизации. Интерфейс IDispatch Интерфейс IDispatch обеспечивает возможность позднего связывания, то есть вызовов методов объектов не по адресам, а по именам на этапе выполнения про- граммы. Интерфейс определен так: type IDispatch = interface!IUnknown) [’{00020400-0000-0000-C000-000000000046}’] function GetTypelnfoCountCout Count: Integer): Integer: stdcall: function GetTypelnfo!Index, LocalelD: Integer: out Typeinfo): Integer: stdcall; function GetIDsOfNames(const IID: TGUID; Names: Pointer: NameCount. LocalelD: Integer; DispIDs: Pointer): Integer; stdcall; function Invoke(DispID: Integer; const IID: TGUID; LocalelD: Integer: Flags: Word: var Params; VarResult. Exceplnfo. ArgErr: Pointer): Integer; stdcal1. end; Ключевыми методами интерфейса являются GetldsOfNames и Invoke. Метод GetldsOfNames Метод GetldsOfNames осуществляет трансляцию имен методов и свойств объекта автоматизации в целочисленные идентификаторы. Рассмотрим ссылку вида: SomeObject.DoSomeThi ng
Интеграция компонента TScriptControl с VCL 467 Если OLE пытается разрешить эту ссылку, то у объекта SomeObject запраши- вается интерфейс IDispatch и вызывается метод GetldsOfNames, которому переда- ются следующие параметры: Ж Names — ссылка на массив имен, требующих разрешения; ж NameCount — количество имен; ж Local eld — региональный контекст. Метод GetldsOfNames должен заполнить массив, на который указывает пара- метр Displds, значениями идентификаторов имен. Объект имеет возможность предоставить разные имена методов для каждого поддерживаемого языка. Если это не требуется, параметр Local eld можно игнорировать. В стандартной реализации интерфейс IDispatch ищет информацию об именах методов и их идентификаторах в библиотеке типов объекта, однако программист вполне может взять эту работу на себя и осуществлять трансляцию самостоя- тельно. Метод Invoke После получения идентификатора запрошенного метода OLE вызывает функцию Invoke, передавая в нее перечисленные ниже параметры. ж DispID — идентификатор вызываемого метода или свойства, полученный от метода GetldsOfNames. Ж Localeld — региональный контекст (тот же, что и в GetldsOfNames). Ж Flags — битовая маска, состоящая из следующих флагов: □ DISPATCH METHOD — вызывается метод (если у объекта есть свойство с таким же именем, то будет установлен также флаг DISPATCH_PROPERTYGET); □ DISPATCH_PROPERTYGET — запрашивается значение свойства; □ DISPATCH_PROPERTYPUT — устанавливается значение свойства; □ DISPATCH_PROPERTYPUTREF — параметр передается по ссылке (если флаг не ус- тановлен — по значению). Ж Params — структура DISPPARAMS, содержащая массив параметров, массив иден- тификаторов для именованных параметров и количества элементов в этих массивах. Параметры передаются в порядке, обратном порядку их следования в функции, как это принято в Visual Basic. й VarResult — адрес переменной типа OleVariant, в которую должен быть поме- щен либо результат вызова метода, либо значение свойства, либо nil, если возвращаемое значение не требуется. Ж Exceplnfo — адрес структуры EXCEPTINFO, которую метод должен заполнить ин- формацией об ошибке, если она возникнет. Ж ArgErr — адрес массива, в который должны быть помещены индексы невер- ных параметров, в случае если такая ситуация будет обнаружена. При вызове метода Invoke не осуществляется никаких проверок, поэтому в слу- чае его самостоятельной реализации необходимо соблюдать аккуратность при работе с переданными адресами массивов и переменных.
468 Глава 10. Microsoft Script Control Как видно из описания IDispatch, имеется возможность самостоятельно реа- лизовать этот интерфейс, динамически преобразуя обращения к объекту автома- тизации в обращения к соответствующим свойствам классов Delphi. Информация RTTI Delphi Delphi имеет свой внутренний протокол RTTI (Run-Time Type Information), позволяющий обращаться к опубликованным (объявленным в секции published) свойствам и методам класса. Этой цели служат функции модуля Typlnfo.pas. Ключевой является следующая функция: function GetPropInfoITypelnfo: PTypelnfo: const PropName: String): PPropInfo: Эта функция позволяет по имени свойства получить адрес структуры PPropInfo, содержащей информацию о свойстве. В дальнейшем можно получить значение этого свойства при помощи функций GetХХХРгор или установить его функциями SetXXXProp. При этом будут корректно вызваны функции получения или установки свойства. Таким образом, у нас есть возможность по имени свойства опреде- лить его наличие и установить или получить его значение. Такая возможность позволяет создать реализацию интерфейса IDispatch, динамически транслирую- щую обращения к свойствам зарегистрированного в TScriptControl объекта авто- матизации в обращения к свойствам связанного с ним экземпляра класса VCL. Класс TVCLProxy Итак, как показано выше, — RTTI Delphi предоставляет достаточную функцио- нальность для того, чтобы обеспечить трансляцию вызовов OLE Automation в об- ращения к свойствам компонентов VCL. Для этого необходимо: ж в методе GetldsOfNames проверить существование свойства при помощи функ- ции GetPropInfo и, если такое свойство найдено, вернуть какой-нибудь число- вой идентификатор (в роли такого идентификатора удобно использовать ре- зультат, возвращаемый функцией GetPropInfo); В в методе Invoke установить или получить значение свойства, используя функ- ции GetXXXProp или SetXXXProp. Для трансляции вызовов OLE в VCL создадим класс TVCLProxy: type // Этот интерфейс понадобится для получения ссылки на // класс VCL из методов, в которые передается его // интерфейс IDispatch IQueryPersistent = interface ['{26F5B6E1-9DA5-11D3-BCAD-00902759A497}’] function GetPersistent: TPersistent: end: TVCLProxy = class(TInterfacedObject. IDispatch. IQueryPersistent)
Интеграция компонента TScriptControl с VCL 469 private FOwner: TPersistent: FScriptControl: TVCLScriptControl; procedure DoCreateControl(AName. AClassName: WideString; WithEvents: Boolean): function SetVCLProperty(PropInfo: PPropInfo: Argument: TVariantArg): HRESULT: function GetVCLProperty(PropInfo: PPropInfo; dps: TDispParams; PDispIds: PDispIdList: var Value: OleVariant): HRESULT: { IDispatch } function GetTypelnfoCountCout Count: Integer): HResult; stdcall; function GetTypelnfo!Index. LocalelD: Integer; out Typeinfo): HResult; stdcall: function GetIDsOfNamesCconst IID: TGUID; Names: Pointer; NameCount. LocalelD: Integer: DispIDs: Pointer): HResult: stdcall: function InvokeCDispID: Integer: const IID: TGUID: LocalelD: Integer: Flags: Word: var Params: VarResult, Exceplnfo. ArgErr: Pointer): HResult: stdcall: { IQueryPersi stent } function GetPersIstent: TPersistent; protected function Dolnvoke (DispID: Integer; const IID: TGUID; LocalelD: Integer: Flags: Word: var dps : TDispParams; pDlspIds : PDispIdList; VarResult, Exceplnfo. ArgErr: Pointer): HResult: virtual: public constructor Create!AOwner: TPersistent: Scriptcontrol: TVCLScriptControl): destructor Destroy; override: end: Экземпляр этого класса создается при регистрации объекта в TScriptControl и уничтожается автоматически, когда потребность в нем исчезает. Поле FOwner хранит ссылку на экземпляр класса VCL, интерфейс к которому предоставляет объект, зарегистрированный в TScriptControl. TVCLScriptControl — это наследник TScriptControl. Главным отличием класса TVCLScriptControl является наличие списка заре- гистрированных экземпляров TVCLProxy и обработчиков событий, позволяющих компонентам VCL вызывать методы сценария.
470 Глава 10. Microsoft Script Control Здесь рассмотрены лишь ключевые моменты реализации; полный код, вместе с примером использования, приведен на прилагаемом компакт-диске. Написание метода GetldsOfNames В методе GetldsOfNames мы должны проверить наличие запрошенного свойства и вернуть адрес его структуры TPropInfo, если такое свойство найдено. function TVCLProxy.GetIDsOfNames(const IID: TGUID; Names: Pointer: NameCount, LocalelD: Integer: DispIDs: Pointer): HResult: var S: String: Info: PPropInfo: begin Result := S_OK; // Получаем имя функции или свойства S := PNamesArray(Names)[O]: // Проверяем, есть ли VCL-свойство с таким же именем Info := GetPropInfoIFOwner.Classinfo. S); if Assigned(Info) then begin // Свойство есть, возвращаем в качестве Displd // адрес структуры Propinfo PDispIdsArray(Displds)[0] := Integer(Info): end Дополним нашу реализацию возможностью вызова некоторых дополнитель- ных функций. Ж Функция Controls для наследников класса TWinControl возвращает ссылку на дочерний компонент с именем или индексом, заданным в параметре. Я Функция Count выполняет следующие действия: □ для компонентов TWi nControl возвращает количество дочерних компонентов; □ для компонентов TCol 1 ecti on возвращает количество элементов; □ для компонентов TStrings возвращает количество строк. « Функция Add выполняет следующие действия: □ для компонентов TWinControl создает дочерний компонент; □ для компонентов TCollection добавляет элемент в коллекцию; □ для компонентов TStrings добавляет строку. й Функция HasProperty возвращает истину, если у объекта есть свойство с задан- ным именем. Для вызова перечисленных выше функций дополним метод GetldsOfNames сле- дующим кодом: el se // Нет такого свойства, проверяем, не имя ли это // одной из определенных нами функций
Интеграция компонента TScriptControl с VCL 471 if CompareText(S, ’CONTROLS’) = 0 then begin if (FOwner is TWinControl) then PDispIdsArray(Displds)[0] := DISPID_CONTROLS else Result := DISP_E_UNKNOWNNAME: end else if CompareText(S. ’COUNT’) = 0 then begin if (FOwner is TCollection) or (FOwner is TStrings) or (FOwner is TWinControl) then PD1spIdsArray(DispIds)[0] := DISPID_COUNT el se Result := DISP_E_UNKNOWNNAME: end el se if CompareText(S. ’ADD’) = 0 then begin Result := S_OK: if (FOwner is TCollection) or (FOwner is TStrings) or (FOwner is TWinControl) then PD1spIdsArray(D1spIds)[0] := DISPID_ADD el se Result := DISP_E_UNKNOWNNAME: end else if Compa reTexUS, 'HASPROPERTY') = 0 then PD1spIdsArray(DispIds)[0] : = DISPID_HASPROPERTY el se Result := DISP_E_UNKNOWNNAME; end: Константы DISPID_CONTROLS, DISPID_COUNT и т. д. определены как целые числа из диапазона от 1 до 1 000 000. Это вполне безопасно, поскольку адрес структуры TPropInfo никак не может оказаться ниже 1 Мбайт. Написание метода Invoke Первая часть задачи выполнена — мы проинформировали OLE о наличии в на- шем сервере автоматизации поддерживаемых функций. Теперь необходимо реа- лизовать метод Invoke для вызова этих функций. Из соображений модульности Invoke выполняет подготовительную работу со списком параметров и вызывает метод Dolnvoke, в котором мы осуществляем трансляцию параметра DispID в обра- щения к методам класса VCL. В методе используются три служебные функции: И CheckArgCount — проверяет количество переданных аргументов; И _ValidType — проверяет соответствие аргумента с заданным индексом задан- ному типу; * _IntValue — получает целое число из аргумента с заданным индексом.
472 Глава 10. Microsoft Script Control function TVCLProxy.DoInvokeCDispID: Integer; const IID: TGUID; LocalelD: Integer; Flags: Word: var dps: TDispParams: pDispIds: PDispIdList: VarResult. Exceplnfo. ArgErr: Pointer ): HResult; var S: String; Put: Boolean: I: Integer: P: TPersistent: B: Boolean: Outvalue: OleVariant: begin Result := S_OK: case Dispid of Для функции Controls мы должны проверить, что передан один параметр. Если он строковый — поиск дочернего компонента будет происходить по имени, в противном случае — по индексу. Если компонент найден, вызывается функция FScriptControl.GetProxy, которая проверяет наличие «представителя» у этого компо- нента, при необходимости создает его и возвращает интерфейс IDispatch. Такой алгоритм необходим для корректной работы оператора i s языка VBScript, кото- рый сравнивает две ссылки на объект и возвращает истину в случае, если речь идет об одном и том же объекте, например: Dim А Dim В Set А = С Set В = С If A is В Then ... Если создавать экземпляр класса TVCLProxy каждый раз, когда запрашивается ссылка, эти экземпляры окажутся разными, и оператор i s не будет работать. DISPID_CONTROLS: begin // Вызвана функция Controls with FOwner as TWinControl do begin // Проверяем параметр CheckArgCountldps.cArgs. [1]. True); P := nil: if _ValidType(O, VT_BSTR, FALSE) then begin // Если параметр - строка, то ищем // дочерний компонент с таким именем S := dps.rgvarg*[pDispIds*[O]].bstrVal: for I := 0 to PredIControlCount) do if CompareTextIS. Controls[I].Name) = 0
Интеграция компонента TScriptControl с VCL 473 then begin P := Controls[IJ; Break: end; end else begin // Если параметр - число, 11 находим компонент по индексу I := JntValue(O): Р := Controls[IJ: end; if not Assigned(P) then // Компонент не найден raise ElnvalidParamType.Create!"); // Возвращаем интерфейс IDispatch 11 для найденного компонента OleVariant(VarResult') := FScriptControl.GetProxy(P); end; end; Функция Count должна вызываться без параметров и возвращать количество элементов в запрашиваемом объекте: DISPID_COUNT: begin // Вызвана функция Count И Проверяем, что не было параметров CheckArgCount(dps.cArgs, [0]. True); if FOwner is TWinControl then // Возвращаем количество дочерних компонентов 01 eVari ant(VarResult") := TWinControl(FOwner).Control Count; else if FOwner is TCollection then // Возвращаем количество элементов коллекции OleVariant(VarResult*) ;= TCollection!FOwner).Count el se if FOwner is TStrings then // Возвращаем количество строк OleVariant(VarResulP) : = TStrings(FOwner).Count; end; Метод Add добавляет элемент к объекту-владельцу «представителя». Обратите внимание на реализацию необязательных параметров для TWinControl и TStrings: DISPID_ADD: begin // Вызвана функция Add if FOwner is TWinControl then begin // Проверяем количество аргументов
474 Глава 10. Microsoft Script Control CheckArgCount(dps.cArgs. [2.3]. True): // Проверяем типы обязательных аргументов _Va11dType(0. VT_BSTR. True): _ValidType(l. VT_BSTR. True): // Третий аргумент - необязательный, если он И не задан, полагаем, что он равен False if (dps.cArgs = 3) and _ValidType(2. VT_BOOL, TRUE) then В := dps.rgvarg*[pDispIds"[O]].vbool else В := False; // Вызываем метод для создания компонента DoCreateControl( dps.rgvarg*[pDisp!ds*[O]J.bstrVal. dps.rgvarg"[pDisplds*[l]].bstrVal, B); end el se if FOwner is TCollection then begin // Добавляем компонент P := TCollection(FOwner).Add; // И возвращаем его интерфейс IDispatch 01eVariant(varResult") := FScriptControl.GetProxy(P); end else if FOwner is TStrings then begin // Проверяем наличие аргументов CheckArgCount(dps.cArgs, [1.2], True): // Проверяем, что аргумент - строка _ValidType(O. VT_BSTR, TRUE); if dps.cArgs = 2 then // Второй аргумент - позиция в списке I := _IntValue(l) el se // Если его нет - вставляем в конец I := TStrings(FOwner).Count; // Добавляем строку TStrings (FOwner). Insertd. dps.rgvarg*[pDisp!dsA[O]].bstrVal): end; end: И наконец, функция HasProperty проверяет наличие у объекта VCL опублико- ванного свойства с заданным именем: DISPID_HASPROPERTY: begin // Вызвана функция HasProperty // Проверяем наличие аргумента CheckArgCount(dps.cArgs. [1]. True):
Интеграция компонента TScriptControl с VCL 475 // Проверяем тип аргумента _ValidType(O. VT_BSTR, True): S := dps.rgvarg"[pDispIds^[O]].bstrVal: // Возвращаем True, если свойство есть OleVariant(varResult') := Assigned(GetPropInfo(FOwner.Classinfo. S)): end: Если ии один из параметров DispID не обработан, значит, DispID содержит адрес структуры TPropInfo свойства VCL: el se // Это не наша функция, значит, это свойство // Проверяем Flags, чтобы узнать, устанавливается // значение свойства или получается Put := (Flags and DISPATCH_PROPERTYPUT) <> 0: if Put then begin // Устанавливаем значение 11 Проверяем наличие аргумента CheckArgCount(dps.cArgs. [1]. True): // И устанавливаем свойство Result := SetVCLProperty(PPropInfo!Displd). dps.rgvarg*[pDispIdsA[0]]) end el se begin // Получаем значение if Dispid = 0 then begin // Dispid = 0 - требуется свойство по умолчанию 11 Возвращаем свой IDispatch 01 eVari ant(VarResultA) := Self as IDispatch: Exit: end; // Получаем значение свойства Result := GetVCLProperty(PPropInfo(DispId), dps. pDispIds. Outvalue): if Result = S_OK then // Получили успешно - сохраняем результат 01 eVaгiant(VarResult*) := Outvalue: end: end: end: Добавление собственных функций Для добавления функций, которые требуются для решения ваших задач, необхо- димо выполнить ряд простых шагов. 1. В методе GetldsOfNames проанализировать имя запрашиваемой функции и опре- делить, может ли она быть вызвана для объекта, на который ссылается FOwner.
476 Глава 10. Microsoft Script Control 2. Если функция может быть вызвана, нужно вернуть уникальный идентификатор DispID, в противном случае — присвоить Result значение DISP_E_UNKNOWNNAME. 3. В методе Invoke необходимо обнаружить свой идентификатор DispID, прове- рить корректность переданных параметров, получить их значения и выпол- нить действие. Обработка событий в компонентах VCL Важным дополнением к реализуемой функциональности является возможность ассоциировать процедуру на VBScript с событием в компоненте VCL, таким как OnEnter, OnClick или OnTimer. Для этого добавим в компонент TVCLScriptControl ме- тоды, которые будут служить обработчиками созданных в коде сценария компо- нентов: TVCLScriptControl = classITScriptControl) publ i shed procedure OnChangeHandler(Sender: TObject): procedure OnClickHandler(Sender: TObject): procedure OnEnterHandlerlSender: TObject); procedure OnExitHandler(Sender: TObject): procedure OnTimerHandler(Sender: TObject): end: В методе DoCreateControl, который вызывается из Dolnvoke при обработке ме- тода Add, реализуем подключение соответствующих обработчиков событий со- здаваемого компонента к созданным методам: procedure TVCLProxy.DoCreateControl (AName. AClassName: WideString: WithEvents: Boolean); procedure SetHandlerlControl: TPersistent; Owner: TObject: Name: String): // Функция устанавливает обработчик события Name // на метод формы с именем Name + 'Handler' var Method: TMethod: Propinfo: PPropInfo: begin // Получаем информацию RTTI Propinfo := GetPropInfo(Control.Classinfo. Name); if Assigned(PropInfo) then begin // Получаем адрес обработчика Method.Code := FScriptControl.MethodAddress(Name + 'Handler'): if Assigned(Method.Code) then begin // Обработчик есть
Интеграция компонента TScriptControl с VCL 477 Method.Data := FScriptControl; // Устанавливаем обработчик SetMethodProp(Control, Propinfo. Method); end; end: end; var ThisClass: TControlClass; C: TComponent: NewOwner: TCustomForm; begin // Назначаем свойство Owner на форму if not (FOwner is TCustomForm) then NewOwner := GetParentForm(FOwner as TControl) else NewOwner := FOwner as TCustomForm: // Получаем класс создаваемого компонента ThisClass := TControlCl ass(GetClass(AC1assName)): // Создаем компонент С := ThisClass.Create(NewOwner); // Назначаем имя C.Name := AName: if C is TControl then // Назначаем свойство Parent TControl(C).Parent := FOwner as TWinControl; if WithEvents then begin // Устанавливаем обработчики SetHandler(C. NewOwner. 'OnClick'); SetHandler(C, NewOwner. 'OnChange'); SetHandler(C. NewOwner. 'OnEnter'); SetHandler(C. NewOwner. 'OnExit'): SetHandler(C. NewOwner. 'OnTimer'): end; // Создаем класс, реализующий интерфейс IDispatch. 11 и добавляем его в пространство имен TScriptControl FScriptControl.Registerclass(AName, С); end; Таким образом, если третьим параметром метода Add будет True, то TVCLScriptControl установит обработчики событий OnClick, OnChange, OnEnter, OnExit и OnTimer на свои методы, реализованные следующим образом: procedure TVCLScriptControl.OnClickHandler( Sender: TObject); begin RunProc((Sender as TComponent).Name + '_' + 'OnClick'); end:
478 Глава 10. Microsoft Script Control Примером использования данной функциональности может служить следую- щий код: Sub MainO Self.Add "Timerl", "TTImer". TRUE With Timerl .Interval = 1000 .Enabled = True End With End Sub Sub Timerl_0nT1mer() Self.Caption = CStr(Time) End Sub Если требуется назначить обработчики событий имеющихся на форме компо- нентов, это может быть сделано в коде: Buttonl.OnClick : = ScriptControll.OnClickHandler; То же самое можно также сделать путем реализации соответствующего метода в методах GetIdsOfNames и Invoke. Получение свойств Для получения свойств классов VCL служит метод GetVCLProperty. В нем осуще- ствляется трансляция типов данных Object Pascal в типы данных OLE: function TVCLProxy.GetVCLPropertylPropInfo: PPropInfo: dps: TDispParams: PDispIds: PDispIdList: var Value: OleVariant): HResult: var I: Integer; S: String: P, Pl: TPersistent; DT: TDateTime; begin Result := S_OK: case PropInfo\PropType\Kind of Для данных строкового и целого типа Delphi осуществляет автоматическую трансляцию: tkString, tkLString. tkWChar. tkWString: // Символьная строка Value := GetStrPropIFOwner. Propinfo): tkChar, tklnteger: // Целое число Value := GetOrdPropIFOwner. Propinfo);
Интеграция компонента TScriptControl с VCL 479 Для перечисленных типов OLE не имеет прямых аналогов. Поэтому для всех типов, кроме Boolean, будем передавать символьную строку с именем соответст- вующей константы. Для Boolean имеется подходящий тип данных, и этот случай необходимо обрабатывать отдельно: tkEnumeration: begin // Проверяем, не Boolean ли это i f Compa reText(PropI nf сЛ.PropType*.Name. 'BOOLEAN') = 0 then // Передаем как Boolean Value := Boolean(GetOrdProp(FOwner. Propinfo)); el se // Остальные передаем как строку Value := GetEnumPropIFOwner. Propinfo): end: Самым сложным случаем является свойство объектного типа. Нормальным поведением будет возвращение интерфейса IDispatch, позволяющего OLE обра- щаться к методам класса, на который ссылается свойство. Однако для некоторых классов, имеющих свойства, установленные по умолчанию, таких как TStrings и TCollection, свойство может быть запрошено с индексом. В этом случае следует выдать соответствующий индексу элемент. В то же время, будучи запрошено без индекса, свойство должно выдать интерфейс IDispatch для работы с экземпляром TCollection или TStrings: tkClass: begi n // Получаем значение свойства Р := TPersistent(GetOrdProp(FOwner, Propinfo)); if Assigned(P) and (P is TCollection) and (dps.cArgs = 1) then begin // Запрошен элемент коллекции He индексом (есть параметр) if Vai1dType(dps.rgvarg'CpDispIds*[0]]. VT_BSTR. FALSE) then begin // Параметр строковый, ищем элемент по свойству // DisplayName S := dps.rgvarg"[pDispIds"[O]].bstrVal: Pl := nil: for I := 0 to Pred(TCollection(P).Count) do if CompareTextIS. TCollection(P).Items[I].DisplayName) = 0 then begin Pl := TCollection(P).Items[I]; Break: end: if Assigned(Pl) then // Найден - возвращаем интерфейс IDispatch
480 Глава 10. Microsoft Script Control Value := FScriptControl.GetProxy(Pl) else 11 He найден Result := DISP_E_MEMBERNOTFOUND; end else begin // Параметр целый, возвращаем // элемент по индексу I := IntValue(dps.rgvarg"[pDispIds^[O]]): if (I >= 0) and (I < TCollection(P).Count) then begin P := TCollection(P).Items[I]: Value := FScriptControl.GetProxy(P); end else Result := DISP_E_MEMBERNOTFOUND; end; end Для класса TStrings результатом будет не интерфейс, а строка, выбранная по имени или по индексу: else if Assigned(P) and (P is TStrings) and (dps.cArgs = 1) then begin // Запрошен элемент из Strings 11 с индексом (есть параметр) if VaiidType(dps.rgvarg*[pDisp!ds*[O]], VT_BSTR, FALSE) then begin // Параметр строковый - возвращаем // значение свойства Values S := dps.rgvarg*[pDisp!dsA[O]].bstrVal: Value := TStrings(P).Values[S]; end else begin // Параметр целый, возвращаем строку по индексу I := IntValue(dps.rgvarg*[pDisp!dsA[O]]): if (I >= 0) and (I < TStrings(P).Count) then Value := TStringsCP)[I] else Result := DISP_E_MEMBERNOTFOUND; end: end el se // Общий случай, возвращаем И интерфейс IDispatch свойства if Assigned(P) then Value := FScriptControl.GetProxy(P) else // Или Unassigned, если свойство = nil Value := Unassigned: end:
Интеграция компонента TScriptControl с VCL 481 У чисел с плавающей точкой также есть особенный тип данных — TDateTime. Его необходимо обрабатывать иначе, чем остальные числа с плавающей точкой, поскольку для него в OLE есть отдельный тип данных — 01 eDate: tkFloat: begi n if (PropInfo'.PropType" = System.Typelnfo(TDateTime)) or (Propinfo*.PropType* = System.Typelnfo(TDate)) then begin // Помещаем значение свойства в промежуточную И переменную типа TDateTime DT := GetFloatPropCFOwner. Propinfo); Value : = DT: end else Value := GetFloatProp(FOwner. Propinfo); end; В случае свойства типа «набор» (Set), не имеющего аналогов в OLE, будем возвращать строку с установленными значениями набора, перечисленными через запятую: tkSet: Value ;= SetToString(Propinfo. GetOrdPropCFOwner. Propinfo)); И, наконец, с типом Variant не возникает никаких сложностей: tkVariant: Value ;= GetVarlantPropIFOwner. Propinfo): el se // Остальные типы не поддерживаются Result := DISP_E_MEMBERNOTFOUND; end; end; Установка свойств Для установки свойств классов VCL служит метод SetVCLProperty. В нем осущест- вляется обратная трансляция типов данных OLE в типы данных Object Pascal: function TVCLProxy.SetVCLProperty(PropInfo: PPropInfo: Argument: TVariantArg): HResult; var I: Integer; S: String; DT: TDateTime: ST: TSys tempi me; IP: IQueryPersistent; begin Result := S_OK; case Propinfo*. PropType*. KI nd of
482 Глава 10. Microsoft Script Control Главным отличием этого метода от SetVCLProperty является необходимость проверки типа данных передаваемого параметра: tkChar. tkString, tkLString. tkWChar, tkWStrlng: begin // Проверяем тип параметра ValidType(Argument. VT_BSTR. TRUE); // И устанавливаем свойство SetStrPropCFOwner. Propinfo. Argument.bstrVal): end: Для целочисленных свойств добавим еще один сервис (если свойство имеет тип TCursor или TColor) — обеспечим трансляцию символьной строки с соответ- ствующим названием константы в целочисленный идентификатор: tklnteger: begin // Проверяем тип свойства на TCursor. TColor, 11 если он совпадает и передано символьное значение. // пытаемся получить его идентификатор i f (Compa reText (Р гор I nfo\ Р ropType". Name. 'TCURSOR') =0) and (Argument.vt = VT_BSTR) then begin if not IdentToCursor(Argument.bstrVal, I) then begin Result := DISP_E_BADVARTYPE: Exit: end; end else i f (CompareText(Proplnfo".PropType".Name. 'TCOLOR') =0) and (Argument.vt = VT_BSTR) then begin if not IdentToColor(Argument.bstrVal. I) then begin Result := DISP_E_BADVARTYPE; Exit: end; end else // Просто цифра I := IntValue(Argument): // Устанавливаем свойство SetOrdProp(FOwner, Propinfo. I): end: Для перечисленных типов, за исключением Boolean, значение передается в виде символьной строки, a Bool еап, как и раньше, обрабатывается отдельно: tkEnumeration: begin // Проверяем на тип Boolean - для него в VBScript И есть отдельный тип данных
Интеграция компонента TScriptControl с VCL 483 i f Compa reText(P горIn foA.PropTypeA.Name, 'BOOLEAN') = 0 then begin // Проверяем тип данных аргумента ValidTypeCArgument. VT_BOOL, True); // Это свойство Boolean - получаем значение SetOrdProp(FOwner, Propinfo. Integer(Argument.vBool)); end else begin // Перечисленный тип передается 11 в виде символьной строки - И проверяем тип данных аргумента ValidType(Argument, VT_BSTR. True): // Получаем значение S := Trim(Argument.bstrVal); SetEnumProp(FOwner. Propinfo, S): end; end: При установке объектного свойства необходимо получить ссылку на класс Delphi, представителем которого является переданный интерфейс IDispatch. Для этой цели служит ранее определенный нами интерфейс IQueryPersi stent. За- просив его у объекта-представителя, мы можем получить ссылку на объект VCL и корректно установить свойство: tkClass: begin // Проверяем тип данных - И должен быть интерфейс IDispatch ValidType(Argument. VTJDISPATCH, TRUE); if Assigned(Argument.dispVal) then begin // Передано непустое значение // Получаем интерфейс IQueryPersistent IP := IDispatch(Argument.dispVal) as IQueryPersistent; // Получаем ссыпку на класс, представителем И которого является интерфейс I := Integer!IP.GetPersistent): end else // Иначе - очищаем свойство I := 0; // Устанавливаем значение SetOrdProp!FOwner. Propinfo. I): end; Для чисел с плавающей точкой основной проблемой является отработка свойства типа TDateTime. Дополнительно обеспечим возможность установить это
484 Глава 10. Microsoft Script Control свойство в виде символьной строки. При установке свойства типа TDateTime не- обходимо обеспечить трансляцию его из формата Т01 eDate в TDateTime: tkFloat: begin if (Propinfo*.PropType* = System.Typelnfo(TDateTime)) or (Propinfo*.PropType* = System.Typelnfo(TDate)) then begin // Проверяем тип данных аргумента if Argument.vt = VT_BSTR then begin DT := StrToDate(Argument.bstrVal): end else begin ValidType(Argument. VT_DATE. True): if VariantTimeToSystemTime(Argument.date, ST) <> 0 then DT := SystemTimeToDateTime(ST) else begin Result := DISP_E_BADVARTYPE; Exit; end: end: SetFloatProp(FOwner, Propinfo, DT); end else begin // Проверяем тип данных аргумента ValidType(Argument. VT_R8, TRUE): // Устанавливаем значение SetFloatProp(FOwner, Propinfo. Argument.dblVai): end; end: Для набора (Set) преобразуем переданную строку в набор при помощи функ- ции StringToSet: tkSet: begin // Проверяем тип данных, должна быть символьная строка ValidType(Argument. VT_BSTR. TRUE): // Получаем данные S := Trim(Argument.bstrVal); // Устанавливаем значение свойства SetOrdProp(FOwner. Propinfo. StringToSet(PropInfo. S)); end; Свойство типа Variant установить несложно: tkVariant: begin // Проверяем тип данных аргумента ValidType(Argument. VT_VARIANT. True); // Устанавливаем значение SetVariantProp(FOwner. Propinfo.
Оператор For Each 485 Argument. pva rVa Ю: end; el se // Остальные типы данных OLE не поддерживаются Result := DISP_E_MEMBERNOTFOUND; end; end; Таким образом, мы реализовали полную функциональность по трансляции вызовов OLE в обращения к свойствам VCL. Наш компонент может динамиче- ски создавать другие компоненты на форме, обращаться к их свойствам и даже обрабатывать возникающие в них события. Оператор For Each Удобным средством, предоставляемым VBScript, является оператор For Each, организующий цикл по всем элементам заданной коллекции. Добавим поддержку этого оператора в наш компонент. Интерфейс lEnumVariant Реализация оператора For Each предусматривает следующее. 1. Исполняющее ядро Scriptcontrol вызывает метод Invoke объекта, по элемен- там которого должен производиться цикл с параметром: DispID = DISPID_NEWENUM (-4) 2. Объект должен вернуть интерфейс lEnumVariant. 3. Далее ядро использует методы lEnumVariant для получения элементов кол- лекции. Интерфейс lEnumVariant определен так: type lEnumVariant = interface!IUnknown) [’{00020404-0000-0000-GOOD - 000000000046}’] function Nextlcelt: LongWord; var rgvar: OleVariant; pceltFetched: PLongWord): HResult: stdcall; function Skiplcelt: LongWord): HResult; stdcall; function Reset: HResult; stdcall; function Clone(out Enum: lEnumVariant): HResult: stdcall: end; В модуле ActiveX.pas в оригинальной поставке Delphi ошибочно определен метод Next: function Next(celt: LongWord: var rgvar: OleVariant; out pceltFetched: LongWord): HResult; stdcall; Для корректной реализации интерфейс должен быть переопределен.
486 Глава 10. Microsoft Script Control Класс TVCLEnumerator Создадим класс, инкапсулирующий функциональность lEnumVariant: type TVCLEnumerator = class(TInterfacedObject. lEnumVariant) private FEnumPosition: Integer: FOwner: TPersistent: FScriptControl: TVCLScriptControl: { lEnumVdridnt } function Next(celt: LongWord: var rgvar: OleVariant: pceltFetched: PLongWord): HResult; stdcall: function Skiplcelt: LongWord): HResult; stdcall; function Reset: HResult: stdcall: function Clone(out Enum: lEnumVariant): HResult; stdcal1; public constructor Create(A0wner: TPersistent; AScriptControl: TVCLScriptControl): end: Конструктор устанавливает свойства FOwner и FScriptControl: constructor TVCLEnumerator.Create(AOwner: TPersistent: AScri ptControl: TVCLScri ptControl): begin inherited Create: FOwner := AOwner; FScriptControl := AScriptControl: FEnumPosition := 0: end: Метод Reset подготавливает реализацию интерфейса к началу перебора: function TVCLEnumerator.Reset: HResult: begin FEnumPosition := 0; Result := S_OK; end; Главная функциональность сосредоточена в методе Next, который получает следующие переменные: И celt — количество запрашиваемых элементов; Я rgvar — адрес первого элемента массива переменных типа OleVariant; К pceltFetched — адрес переменной, в которую должно быть записано количество реально переданных элементов (этот адрес может быть равен nil, в этом случае не потребуется ничего записывать).
Оператор For Each 487 Метод должен заполнить запрошенное количество элементов rgvar и вернуть S_OK, если это удалось, и S_FALSE, если элементов не хватило: type TVariantList = array [0..0] of OleVariant: function TVCLEnumerator.Next(celt: LongWord: var rgvar: OleVariant: pceltFetched: PLongWord): HResult; var I: Cardinal; begin Result ;= S_OK; I := 0; Для объекта TWinControl возвращаем интерфейсы IDispatch для компонентов из свойства Controls: if FOwner is TWinControl then begin with TWinControl(FOwner) do begin while (FEnumPosition < Control Count) and (I < celt) do begin TVariantList(rgvar)[I] := FScriptControl.GetProxy(Controls[FEnumPosition]); Inc(I): Inc(FEnumPosition): end; end: end Для объекта TCol lection организуется перебор элементов коллекции: else if FOwner is TCollection then begin with TCollection(FOwner) do begin while (FEnumPosition < Count) and (I < celt) do begin TVariantList(rgvar)[I] := FScriptControl.GetProxy(Items[FEnumPosition]); Inc(I); Inc(FEnumPosition); end: end; end Для объекта TStrings перебираются строки и возвращаются их значения: else if FOwner is TStrings then begin with TStrings(FOwner) do begin while (FEnumPosition < Count) and (I < celt) do begin
488 Глава 10. Microsoft Script Control TVariantList(rgvar)[I] : = TStr1ngs(FOwner)[FEnumPosi ti on]; Inc(I): Inc(FEnumPosition); end; end; end else Result := S_FALSE; if I <> celt then Result := SJALSE: if Assigned(pceltFetched) then pceltFetched* : = I; end; Метод Skip пропускает запрошенное количество элементов и возвращает S OK, если еще остались элементы для перебора: function TVCLEnumerator.Skip(celt: LongWord); HResult; var Total; Integer; begin Result := SJALSE; if FOwner is TWinControl then Total := TWinControl(FOwner).ControlCount else if FOwner is TCollection then Total := TCollection(FOwner).Count else if FOwner is TStrings then Total := TStrings(FOwner).Count else Exit: if FEnumPosition + celt <= Total then begin Result := S_OK; Inc(FEnumPosition. celt) end; end; Метод Cl one клонирует объект, возвращая интерфейс его клона (копии): function TVCLEnumerator.Clone(out Enum: lEnumVariant): HResult; var NewEnum: TVCLEnumerator; begin NewEnum := TVCLEnumerator.Create(FOwner. FScriptControl): NewEnum.FEnumPosition : = FEnumPosition; Enum := NewEnum as lEnumVariant: Result := S_OK; end;
Компонент TVCLScriptControl 489 Для того чтобы класс TVCLProxy мог вернуть интерфейс lEnumVariant, требуется дополнить метод Invoke следующим кодом: case Displd of DISPID_NEWENUM: begin // У объекта запрашивают интерфейс II lEnumVariant для цикла For Each 11 создаем класс, реализующий этот интерфейс 01 eVariant(VarResult*) : = TVCLEnumerator.Create!FOwner. FScriptControl) as lEnumVariant: end: Компонент TVCLScriptControl Текст компонента TVCLScriptControl приведен на прилагаемом компакт-диске. Данный компонент является наследником компонента TScriptControl и реализует функциональность по работе с классом TVCLProxy. На компакт-диске имеется демонстрационный пример использования компо- нента TVCLScriptControl. В этом примере выполняется код на языке VBScript, ко- торый помещен в компонент ТМето. Приведены примеры команд для изменения свойств, обработки событий, динамического создания визуальных компонентов VCL. С помощью предлагаемого кода можно динамически создавать компоненты TButton, TPanel, TEdit, TLabel, TTimer, TDateTimePicker (и более никаких). Причина этого заключается в том, что компилятор Delphi производит оптимизацию: если в проекте не используется какой-либо класс, то все методы и переменные это- го класса не включаются в ЕХЕ-файл. Если попытаться динамически создать класс, для которого отсутствует код в ЕХЕ-файле, то в результате получим исключение. Для того чтобы изменить оптимизационные настройки компилятора, исполь- зуется метод Registerclasses: Registerclasses([TButton, TPanel. TEdit. TLabel. TTimer. TDateTimePicker]); Этот метод изменяет оптимизационный процесс, а именно — для описанных классов и их предков в ЕХЕ-файл будут включены все методы и все переменные. Даже если класс используется в проекте, рекомендуется его включать в метод Registerclasses — иначе компилятор удалит статические методы, вызовы кото- рых отсутствуют в коде приложения. Таким образом, для работы с другими классами достаточно просто вызвать для них метод Registerclasses. Например, если данный проект будет использо- ваться для создания другого, где предполагается работа с компонентом TBitBtn, вызов метода RegisterCl asses будет следующим: Registerclasses([TButton. TPanel, TEdit. TLabel. TTimer. TDateTimePicker. TBitBtn]);
490 Глава 10. Microsoft Script Control Заключение Интерпретатор Microsoft Script Control предлагает качественное решение задач, требующих включения в программу интерпретирующего ядра. Интегрировав его с VCL, мы получаем мощный и гибкий инструмент, позволяющий наращивать возможности в любом направлении. Приведенной в данной главе информации вполне достаточно, чтобы на основе имеющегося на прилагаемом компакт-диске компонента TVCLScriptControl создать решение, удовлетворяющее любой конкрет- ной задаче. На этом мы заканчиваем обсуждение вопросов применения СОМ-объектов, входящих в состав Windows. Теперь настало время перейти к вопросам создания распределенных приложений. Этой теме, вызывающей неослабевающий интерес разработчиков последние несколько лет, посвящены следующие несколько глав книги.
ГЛАВА 11 Удаленный доступ к серверам автоматизации В предыдущих главах мы обсудили практически все аспекты, связанные с созда- нием серверов и контроллеров автоматизации и их применением в случае, когда и сервер, и контроллер выполняются на одном и том же компьютере. Отметим, однако, что это не единственный, а иногда и не самый лучший способ автоматиза- ции — во многих случаях сервер и контроллер выполняются на разных компью- терах. Ниже мы обсудим способы реализации подобного удаленного доступа к сер- верам автоматизации. Маршалинг и удаленный доступ к СОМ-серверам Как уже было сказано в главе 1, серверы автоматизации могут выполняться как в адресном пространстве контроллера (такие серверы называются внутрипроцесс- ными и реализуются в виде динамически загружаемых библиотек), так и в собствен- ном адресном пространстве (такие серверы называются внепроцессными и реали- зуются в виде исполняемых файлов). Отметим, что и внутрипроцессный, и внепроцессный COM-сервер можно пре- вратить в удаленный (то есть запускаемый на компьютере, отличном от компью- тера, содержащего контроллер). Такой способ использования серверов особенно удобен в случае, когда сервер требует для своей работы особых ресурсов, недоступ- ных на всех компьютерах, содержащих контроллеры, таких как дополнительный объем оперативной памяти, уникальное оборудование, дополнительное программ- ное обеспечение, требующее сложной конфигурации, настройки и поддержки. Нередко удаленный доступ применяется и в тех случаях, когда это выгодно с точки зрения схемы лицензирования сервера, например, когда лицензия на использо- вание сервера или каких-либо применяемых им сервисов требуется для каждого компьютера, на котором он установлен, но при этом в лицензионном соглашении не указаны ограничения на число клиентов, подключаемых к серверу (типичный пример такого лицензирования — лицензирование приложений Microsoft Office), либо когда стоимость серверной лицензии существенно превышает стоимость клиентской лицензии. Один из часто встречающихся примеров практического применения удален- ного доступа — удаленный запуск какого-либо из приложений Microsoft Office
492 Глава 11. Удаленный доступ к серверам автоматизации (например, Microsoft Excel), установленного на единственном компьютере локаль- ной сети, для печати документов, генерируемых клиентскими приложениями. В этом случае можно не только сэкономить средства, которые пришлось бы затратить на приобретение нескольких копий Excel, но и снизить требования к ресурсам рабочих станций, содержащих приложения-контроллеры Excel — ведь приложение выполняется в оперативной памяти той рабочей станции, на которой оно установлено. Другой пример, вызывающий в последнее время немалый интерес разра- ботчиков информационных систем, — это трехзвенная информационная система с тонким (thin), или облегченным, клиентом (контроллером автоматизации) и сер- вером автоматизации (в данном случае он называется сервером доступа к данным), предоставляющим топкому клиенту сервисы, связанные с доступом к данным, содержащимся, в свою очередь, в какой-либо серверной СУБД (вопросам создания серверов доступа к данным посвящена глава 12). В этом случае, помимо экономии ресурсов компьютеров, содержащих контроллеры, имеется еще одно преимуще- ство, связанное с тем, что в общем случае для доступа к данным требуется про- граммное обеспечение, нуждающееся в отдельной установке, сложной настройке и поддержке. В качестве такого программного обеспечения могут выступать кли- ентская часть серверной СУБД и библиотеки, реализующие универсальные механизмы доступа к данным. Если это программное обеспечение используется только сервером, но не используется контроллерами, сопровождение такого рас- пределенного приложения оказывается менее затратным, нежели сопровождение стандартного приложения в архитектуре «клиент-сервер». При рассмотрении работы удаленных серверов возникает естественный во- прос: каким образом контроллер обращается к свойствам и методам объектов, расположенных за пределами его адресного пространства? Для этого вспомним, каким образом вообще взаимодействуют COM-клиент и СОМ-сервер. При обращении COM-клиента к COM-серверу происходит поиск его место- положения в реестре Windows. Если сервер является внутрипроцессным, то есть выполненным в виде DLL, эта библиотека загружается в адресное пространство клиента. Если сервер внепроцессный, используется функция CreateProcess, кото- рая загружает исполняемый файл сервера в отдельное адресное пространство. При этом в адресных пространствах клиента и сервера создаются прокси и стаб, а для передачи данных между этими объектами используется маршалинг и де- маршалинг, то есть упаковка данных с помощью функции CoMarshal Interface и их распаковка с помощью функции CoUnMarshal Interface. Именно таким образом ра- ботают совместно внепроцессный сервер автоматизации и его контроллер, и по- скольку их взаимодействие заключается в загрузке сервера в оперативную па- мять того же самого компьютера, на котором выполняется контроллер, никаких особых противоречий с точки зрения безопасности данных не возникает. Если же сервер и контроллер располагаются на разных компьютерах, тут ситуа- ция несколько иная. Во-первых, контроллер автоматизации должен «знать», на каком компьютере находится сервер, и эти сведения должны содержаться либо внутри самого приложения-контроллера, либо в настройках тех сервисов, с помо- щью которых осуществляется удаленный доступ. Во-вторых, вполне очевидно, что в общем случае при наличии сервера автоматизации, установленного и заре-
Удаленный доступ с помощью сервисов DCOM 493 гистрированного на каком-либо компьютере, произвольный пользователь другого компьютера (даже члена того же самого домена или той же самой рабочей груп- пы) не должен иметь возможности его запускать. Это диктуется элементарными соображениями безопасности — было бы совершенно недопустимо, если бы любой пользователь сети мог запустить любой СОМ-сервер на любом из компьютеров, доступных в этой сети. Именно поэтому пользователем компьютера, содержащего сервер, должно предоставляться разрешение на его удаленный запуск с других компьютеров согласно тем или иным правилам. Как можно технически реализовать подобное разрешение? В общем случае имеется два способа. Первый заключается в применении сервисов DCOM (Distri- buted СОМ), когда вся работа по поиску и удаленному запуску приложения возла- гается на сервисы операционной системы. Второй способ заключается в примене- нии универсального COM-клиента, который, с одной стороны, способен запускать любой сервер (или любой из какой-то определенной категории серверов) и вызы- вать любой его метод, а с другой стороны, может взаимодействовать с контрол- лером с помощью какого-то сетевого протокола, например TCP/IP или HTTP. Ниже мы рассмотрим оба эти способа. Удаленный доступ с помощью сервисов DCOM Использование технологии DCOM базируется на предоставлении прав на уда- ленный запуск конкретного приложения тем или иным пользователям или груп- пам пользователей. Поэтому в реестре компьютера, содержащего сервер, который планируется запускать удаленно, должны быть описаны права доступа к данному серверу различных пользователей или их групп. Настройка доступа Для конфигурирования DCOM существует утилита DCOMCNFG, входящая в ком- плект поставки Windows NT, Windows 2000 и Windows ХР (запустить ее можно из командной строки). Для Windows 95/98 поддержка DCOM устанавливается отдельно, и соответствующую утилиту для этих операционных систем можно найти на web-сайте компании Microsoft (http://www.microsoft.com). Следует иметь в виду, что в качестве DCOM-клиента часто используют Windows 95, хотя способ применения этой операционной системы в качестве DCOM-сервера также извес- тен (см., например, http://www.distribucon.com/dcom95.html). Для настройки удаленного доступа с помощью сервисов DCOM нужно в пер- вую очередь экспортировать список пользователей сети с первичного контроллера домена. Дело в том, что применение технологии DCOM базируется на предостав- лении прав на удаленный запуск конкретного приложения тем или иным пользова- телям или группам пользователей. Поэтому компьютер, содержащий сервер, кото- рый планируется запускать удаленно, обязательно должен иметь в своем реестре описание прав доступа к данному серверу различных пользователей или их групп. При этом пользователь компьютера, на котором предполагается запускать сервер, должен иметь право импортировать список пользователей домена.
494 Глава 11. Удаленный доступ к серверам автоматизации При таком способе доступа к серверу необходимо, чтобы все клиенты и сам сервер располагались внутри одного домена. Получив список пользователей и групп пользователей домена, с помощью ути- литы DCOMCNFG следует предоставить права на запуск сервера тем или иным пользователям. Запустив эту утилиту, в случае Windows NT/2000 находят нужный сервер в списке всех COM-серверов, зарегистрированных на данном компьютере (рис. 11.1), и затем задают правила использования выбранного сервера. Рис. 11.1. Выбор имени сервера в окне утилиты DCOMCNFG (Windows NT/2000) В случае Windows ХР выполнение команды DCOMCNFG приводит к запуску средства управления службами компонентов Component Services Explorer, и для на- стройки доступа к приложениям с помощью DCOM следует найти раздел Console Root/Component Services/Computers/My Computer/DCOM Config — в нем находится список всех зарегистрированных на данном компьютере COM-серверов (рис. 11.2). Для задания правила использования выбранного сервера в Windows NT/2000 нужно щелкнуть на кнопке Properties (в случае Windows ХР нужно выбрать команду Properties в контекстном меню соответствующего сервера), в открыв- шемся диалоговом окне перейти на вкладку Location и установить флажок Run application on this computer (рис. 11.3). Далее нужно перейти на вкладку Security, установить переключатель Use custom access permission и щелкнуть на находящейся рядом кнопке Edit (рис. 11.4). Это позволит нам выбрать пользователей и группы пользователей, которые смогут
Удаленный доступ с помощью сервисов DCOM 495 подключиться к уже запущенному серверу (из этого разрешения вовсе не следует, что они могут инициировать его запуск). Рис. 11.2. Выбор имени сервера в окне утилиты DCOMCNFG (Windows ХР) Рис. 11.3. Выбор компьютера, на котором выполняется сервер
496 Глава 11. Удаленный доступ к серверам автоматизации Рис. 11.4. Выбор параметров разрешений на доступ к серверу Затем в открывшемся окне со списком пользователей (рис. 11.5, 11.6), уже имеющих это разрешение (он может быть и пуст), следует щелкнуть на кнопке Add и выбрать в списке пользователей домена (рабочей группы) того пользовате- ля (или группу пользователей), которому нужно дать подобное разрешение. dd User* and Group* List Names Franr |©C\MAINDESK* Names- Add Members... i Search A^d Names MAINOESr.\Guesls.MAINOESK\lliSR_MAINOESK 3 I Iypevl«ccess. | Allow Access 3 Рис. 11.5. Выбор пользователей для предоставления им доступа к COM-серверу (Windows NT/2000)
Удаленный доступ с помощью сервисов DCOM 497 Рис. 11.6. Выбор пользователей для предоставления им доступа к COM-серверу (Windows ХР) После этого соответствующие пользователи и группы пользователей будут добавлены в список тех, кому разрешен доступ к уже запущенному серверу (рис. 11.7, 11.8). Рис. 11.7. Список пользователей, имеющих права доступа к COM-серверу (Windows NT/2000) Чтобы разрешить пользователю или группе пользователей не только удаленно обращаться к запущенному серверу, но и инициировать его запуск, в окне задания параметров разрешений (см. рис. 11.4) следует установить переключатель Use custom launch permissions, щелкнуть на находящейся рядом кнопке Edit и повто- рить описанные выше шаги. С помощью переключателя Use custom configuration permissions и соответствующей кнопки Edit можно указать, кому из пользовате- лей разрешено менять настройки данного сервера, если таковые имеются. Чтобы окно сервера отображалось на экране и был доступен пользователь- ский интерфейс этого сервера, следует в окне задания параметров разрешений (см. рис. 11.4) перейти на вкладку Identity и установить переключатель The interactive user (рис. 11.9). В этом случае приложение будет запущено от имени пользователя,
498 Глава 11. Удаленный доступ к серверам автоматизации зарегистрированного на данном компьютере, и именно этому пользователю будут доступны экран и внешние устройства — мышь, клавиатура. Access Permission . Security | group or usef names: C Administrator (CH!LD\AdmmtratorJ ffi SYSTEM OK ! Cancel ! Рис. 11.8. Список пользователей, имеющих права доступа к COM-серверу (Windows ХР) Рис. 11.9. Выбор пользователя, от имени которого запускается сервер
Удаленный доступ с помощью сервисов DCOM 499 Во всех остальных случаях кнопка приложения, сигнализирующая о наличии сервера, на панели задач отображаться не будет, пользовательский интерфейс сервера окажется недоступным, то есть его формы на экране не появятся, а ко- манды от мыши и клавиатуры не будут выполняться. При этом если в процессе работы сервера потребуется вывести модальное диалоговое окно, создастся впечат- ление «зависания» приложения (на экране окна не видно, закрыть его с помощью мыши и клавиатуры нельзя, кнопки приложения на панели задач нет). Выгрузить такое приложение можно, только уничтожив его процесс с помощью утилиты Task Manager, что удается далеко не всегда — ведь владельцем процесса являет- ся другой пользователь. ПРИМЕЧАНИЕ -------------------------------------------------------- В русифицированных версиях Windows NT/2000/XP названия вкладок и элементов управления могут быть другими. Далее следует зарегистрировать сервер в реестре рабочей станции, где будет запускаться клиентское приложение. Для этого сервер автоматизации нужно хотя бы один раз запустить на компьютере, содержащем приложение-клиент. Альтер- нативой является внесение записей о сервере в реестр каким-либо иным спосо- бом, например путем написания процедуры регистрации сервера на клиентской рабочей станции или импорта REG-файлов. В реестре компьютера, на котором запускается контроллер автоматизации, также должны содержаться сведения о том, что соответствующий сервер авто- матизации запускается удаленно. Для этого нужно, чтобы в реестре о сервере автоматизации уже была соответствующая запись, поэтому его требуется хотя бы один раз запустить локально (или перенести соответствующие записи из реестра компьютера, содержащего сервер, на компьютер, содержащий кон- троллер). После этого можно с помощью той же утилиты DCOMCNFG указать, что вы- бранный COM-сервер запускается не на данном компьютере, а на удаленном (рис. 11.10). После этого можно попытаться запустить контроллер и обратиться к серверу. Сервер запустится на удаленном компьютере. Итак, принцип осуществления доступа к COM-серверу с помощью сервисов DCOM можно кратко сформулировать так: «Доступ к серверу возможен для пользователей, имеющих разрешение на этот доступ, а запуск этого сервера — для пользователей, имеющих разрешение на его запуск, причем с компьюте- ров, на которых имеется клиентская часть DCOM и зарегистрирован данный сервер». Применение компонента TDCOMConnection Отметим, что есть способ, позволяющий избежать настройки конфигурации DCOM на клиентском компьютере. Он заключается в использовании компо- нента TDCOMConnection Delphi, доступного в редакции Enterprise и находящегося
500 Глава 11. Удаленный доступ к серверам автоматизации на странице DataSnap палитры компонентов (в Delphi 4 и 5 его можно было найти на странице Midas). Рис. 11.10. Настройка клиентской части DCOM Проиллюстрируем применение компонента TDCOMConnection на несложном примере, создав простейший контроллер автоматизации какого-либо из при- ложений Microsoft Office, например Microsoft Excel. Для этого создадим но- вый проект, поместим на его главную форму кнопку и компонент TDCOMConnection (рис. 11.11). Рис. 11.11. Приложение для тестирования удаленного доступа Установим свойство ServerName компонента DCOMConnecti onl равным программ- ному идентификатору (ProgID) сервера автоматизации — в данном случае это Excel .Application. Если сервер зарегистрирован на данном компьютере и его идентификатор ProgID указан правильно, в свойстве ServerGUID появится GUID
Удаленный доступ с помощью сервисов DCOM 501 этого СОМ-сервера (рис. 11.12). В данном случае это значение считывается из реестра компьютера, на котором производится разработка. Рис. 11.12. Установка свойств компонента TDCOMConnection Далее следует установить свойство ComputerName, введя его вручную или выбрав имя нужного компьютера в диалоговом окне Browse for Computer (рис. 11.13). ! Browse for Computer Select Remote Server Э My Network Places В <“(' Entire Network Й -J* Microsoft Windows Network Й Home t Child ® Workgroup EE Of Child_C on Child It; A Computers Near Me ф h on Child Ф 4^ My Web Sites on MSN S; -Д myweb on maindesk OK Cancel Рис. 11.13. Диалоговое окно Browse for Computer
502 Глава 11. Удаленный доступ к серверам автоматизации Отметим, что пользователь компьютера, на котором предполагается запус- тить сервер автоматизации, должен дать разрешение пользователю компьютера, на котором ведется разработка контроллера, на удаленный запуск сервера (а сам сервер, естественно, должен быть на этом компьютере установлен — иначе запус- кать будет просто нечего). Создадим обработчик события, связанного со щелчком на кнопке: procedure TForml.ButtonlClick(Sender: TObject); var App: Variant; begin DCOMConnecti on 1. Connected := True; App := DCOMConnectionl.AppServer: App.Visible := True: App.Workbooks.Add: // и так далее... end; Запустив созданное приложение, мы увидим, что оно инициирует запуск сервера на компьютере, указанном в свойстве ComputerName компонента DCOMConnectionl при условии, что данный пользователь имеет право запустить его удаленно. Так как модель программирования и механизмы обмена данными в СОМ и DCOM абсолютно одинаковы, с точки зрения сервиса удаленного доступа DCOM локальный запуск сервера есть просто обычный доступ к COM-серверу. Поэтому использование компонента DCOMConnection в клиентском приложении, располо- женном на том же компьютере, что и сервер, приведет к тому, что клиент будет корректно получать данные с сервера, даже если компьютер вообще не поддер- живает DCOM. Прежде чем завершить обсуждение удаленного доступа к серверам автомати- зации с помощью сервисов DCOM, хотелось бы особо обратить внимание на достоинства и недостатки использования DCOM в качестве технологии удален- ного доступа. Несомненным достоинством этого способа является то, что сервисы DCOM интегрированы в операционную систему Windows NT/2000/XP и не тре- буют установки и запуска каких-либо утилит для своего функционирования. Однако этот способ удаленного запуска серверов автоматизации имеет и опреде- ленные недостатки. Компьютер, на котором запускается сервер, должен иметь доступ к спискам всех пользователей сети. И Сервер автоматизации нужно хотя бы один раз запустить на компьютере с контроллером для его регистрации, из чего следует, что он должен быть переносимым между платформами сервера и клиента. Альтернативой явля- ется внесение записей о сервере в реестр каким-либо иным способом, напри- мер путем написания процедуры регистрации сервера на клиентской рабочей станции. Отметим, однако, что удаленный доступ можно осуществить не только с по- мощью DCOM. Далее мы обсудим альтернативные способы осуществления уда- ленного доступа к СОМ-серверам.
Удаленный доступ с помощью протокола TCP/IP 503 Удаленный доступ с помощью протокола TCP/IP Удаленный доступ к COM-серверам с помощью протокола TCP/IP обычно реа- лизуется путем создания COM-клиента, который располагается на том же компь- ютере, что и сервер, и может инициировать его запуск и вызывать его методы. Удаленный контроллер автоматизации при этом обращается не к серверу, а к соз- данному COM-клиенту, передавая ему имя сервера, имена методов и их параметры и получая в ответ результаты их выполнения. Таким образом, никакие сервисы DCOM в этом случае не используются — COM-клиент и COM-сервер выполня- ются на одном и том же компьютере. Вариантов реализации подобных клиентов может быть довольно много. Их можно создавать самим, а можно применять готовые. Одной из первых таких реализаций (ныне ставших уже историей) был продукт OLEnterprise, входивший в комплект поставки Delphi 3 в редакции Client/Server и Delphi 4 в редакции Enterprise. Утилита Object Factory, входящая в состав этого продукта, представляла собой универсальный COM-клиент, который всем пользователям, имеющим установленную клиентскую часть OLEnterprise, предоставлял доступ к фиксиро- ванному набору COM-серверов. Чтобы COM-сервер был доступен для удален- ного запуска, требовалось наличие специальных ключей в реестре сервера и кли- ента, а для их создания в состав OLEnterprise входила специальная утилита Object Explorer, используемая на компьютере, содержащем COM-сервер, и на компьютерах, содержащих клиентские приложения. Borland Socket Server В состав всех версий Delphi, начиная с 3.01, входит другой универсальный СОМ- клиент — Borland Socket Server (файл scktsrvr.exe в каталоге Delphi\Bin). Версии этого приложения, входившие в состав Delphi 3 и 4, будучи запущенными на каком-либо компьютере, позволяли осуществить доступ к любым СОМ-серверам, причем с любого удаленного компьютера, который мог обращаться к данному компьютеру по протоколу TCP/IP (в общем случае не только через локальную сеть, но и через Интернет). При этом, естественно, к компьютеру, содержащему клиентское приложение, не предъявлялось практически никаких требований, кроме собственно поддержки протокола TCP/IP и возможности доступа по указанному порту к компьютеру, содержащему Socket Server. Очевидно, что подобное Socket Server приложение, будучи запущенным на серверном компью- тере, представляло собой серьезную угрозу безопасности данных, поэтому поль- зоваться им следовало очень осторожно. Версии Borland Socket Server, входящие в состав Delphi 5 и выше, были слегка усовершенствованы. Эти приложения можно запускать в двух режимах — предо- ставления доступа ко всем СОМ-серверам, как в прежних версиях Socket Server, и предоставления доступа к ограниченному набору серверов, специальным обра- зом зарегистрированных в реестре.
504 Глава 11. Удаленный доступ к серверам автоматизации Загрузить Socket Server можно как исполняемый файл, просто запустив испол- няемый файл scktsrvr.exe из каталога Delphi\Bin, или зарегистрировать его как сер- вис Windows NT или Windows 2000 (с помощью команды scktsrvr.exe /install). После запуска на панели задач появится соответствующая кнопка, при щелчке на которой открывается окно приложения Socket Server (рис. 11.14). ho . Port 9<Borl.ir»d Socket Server Thread Caching.... Thread Cache Size: Thread Cache Size is the maximum number of threads that can be reused for new cfent connections. Listen on Port: Many values of Port are associated by convention with a particular service such as ftp or http. Port is the ID of the connection on which the server listens for client requests Intercept GUID..................".........-.............j I I _ __ | Intercepr GUID is the GUID for a data interceptor COM object ’ See help for the TSocketConnectron for details. Timeout'.............g~~~*...—"*•.......»» Inactive Timeout: |o “J Inactive Timeout specifes the numbe of minutes a client can be inactive before being disconnected. (0 indicates infinite) Boris Connections Port If Properties | (jses | Рис. 11.14. Borland Socket Server С помощью окна этого приложения можно указывать номера портов, по кото- рым следует вести обмен данными с удаленными клиентами, а также управлять доступом к COM-серверам. Режим доступа можно задать, установив или сняв галочку у команды-переключателя Registered Objects Only в меню Connections. При изменении режима доступа Socket Server следует перезапустить. Borland Socket Server в отличие от DCOM не требует никаких клиентских частей и дополнительных настроек на рабочих станциях, на которых предполага- ется использовать контроллер, и не делает относительно их никаких предполо- жений (кроме, естественно, того, что рабочие станции оснащены 32-разрядной версией Windows). Как универсальный COM-клиент это и подобные ему приложе- ния идеальны для удаленного доступа к серверам автоматизации через Интернет или с использованием технологий, применяемых в Интернете. В этом случае очень важно избегать каких бы то ни было действий, связанных с установкой и конфигу-
Удаленный доступ с помощью протокола TCP/IP 505 рированием дополнительного программного обеспечения на компьютерах, которые могут содержать контроллеры (естественно, установки самих контроллеров при этом избежать не удастся, но они могут быть выполнены в виде элементов управле- ния ActiveX или в виде дистрибутивов, устанавливающихся с web-страниц, что практически решает проблемы их поставки и конфигурирования). Принцип доступа к COM-серверу по протоколу TCP/IP можно кратко сформу- лировать так: «Доступ к COM-серверу возможен со всех удаленных компьютеров, имеющих доступ по протоколу TCP/IP через указанный порт к содержащему этот сервер компьютеру при условии, что на этом компьютере запущено приложе- ние Socket Server или эквивалентный ему по функциональности СОМ-клиент, и настройки этого клиента позволяют осуществлять удаленный доступ к данному серверу». Применение компонента TSocketConnection Создать клиентское приложение, использующее доступ по протоколу TCP/IP, несложно. Для этой цели обычно применяется компонент TSocketConnection. Про- иллюстрируем его применение на несложном примере, создав, как и в предыду- щем случае, простейший контроллер автоматизации Microsoft Excel. Для этого создадим новый проект, поместим на его главную форму кнопку и компонент TSocketConnection. Установим его свойство Address равным IP-адресу компьютера, на котором запущено приложение Borland Socket Server (можно вместо этого указать имя этого компьютера в качестве значения свойства Host). Установим свойство ServerName компонента SocketConnectionl равным программному иденти- фикатору сервера автоматизации — Excel .Application. Если сервер зарегистрирован на данном компьютере и идентификатор ProgID указан правильно, в свойстве ServerGUID появится GUID данного СОМ-сервера. Однако в этом случае чте- ние GUID будет производиться уже не из реестра клиентского компьютера, как в случае применения компонента TDCOMConnection, а из реестра компьютера, на котором запущено приложение Socket Server — именно Socket Server и возвращает этот идентификатор GUID. Создадим обработчик события, связанного со щелчком на кнопке: procedure TForml.ButtonlCl1ck(Sender: TObject); var App: Variant: begin SocketConnectionl.Connected := True: App := SocketConnectionl.AppServer; App.Visible := True: App.Workbooks.Add; // и так далее... end: Теперь, если скомпилировать и запустить приложение, а затем на его главной форме щелкнуть на кнопке, произойдет обращение к приложению Socket Server,
506 Глава 11. Удаленный доступ к серверам автоматизации которое, в свою очередь, запустит сервер автоматизации и вызовет методы, на- звания и параметры которых переданы ему клиентом. В данном примере мы использовали режим работы Socket Server, позволяю- щий обращаться к произвольным серверам автоматизации. Если же вернуться к режиму работы, установленному по умолчанию, мы сможем с помощью этого универсального клиента обращаться только к серверам, определенным образом зарегистрированным. Таковыми могут быть DataSnap-серверы; при их регистрации создаются определенные ключи реестра, на основании которых Socket Server предоставляет к ним доступ. О DataSnap-серверах пойдет речь в следующей главе, здесь же мы только отметим, что при создании СОМ-объектов, применяемых в таких серверах, с помощью предназначенных для этого мастеров, к их методу UpdateRegi stry автоматически добавляется процедура EnableSocketTransport, соз- дающая в реестре ключи, с помощью которых Socket Server проверяет, могут ли клиенты обращаться к данному серверу по этому протоколу. Эти ключи могут быть удалены процедурой DisableSocketTransport. Удалив соответствующие вы- зовы из процедуры UpdateRegi stry, мы можем запретить доступ по протоколу TCP/IP к данному DataSnap-серверу, но при этом он может быть доступен по- средством других протоколов. Начиная с Delphi 5, компонент TSocketConnection обладает свойством Supportcallbacks. Если его значение равно True, клиентское приложение прини- мает нотификационные сообщения от сервера. Нотификационные сообщения очень важны в случае многопользовательской работы, например, когда нужно уведомлять клиентов об изменениях, внесенных в общие данные другими кли- ентами. Отметим, что в случае применения компонента TSocketConnection при значении его свойства Supportcallback равным True следует иметь установленную библиотеку Winsock2, доступную во всех 32-разрядных версиях Windows, кроме Windows 95. Для Windows 95 эту библиотеку можно найти на web-сайте Micro- soft по адресу http.7/www.microsoft.com/windows95/downloads/. Если же мы не используем функции обратного вызова этого компонента, можно просто установить свойство Supportcallback равным False, тогда приложе- ние не будет зависеть от наличия или отсутствия библиотеки Winsock2. В заключение обратим внимание на еще одну проблему, связанную с удален- ным доступом по протоколу TCP/IP. Эта проблема, проявляющаяся в том, что соединение между клиентом и сервером не устанавливается при, казалось бы, корректных настройках, может заключаться в неверном предположении о реаль- ном IP-адресе компьютера, содержащего сервер. Дело в том, что в некоторых се- тях, обычно содержащих одновременно Windows-серверы и UNIX-серверы, мо- гут возникнуть проблемы, связанные с наличием на многих рабочих станциях по паре сетевых имен и IP-адресов — одного для сети Microsoft, другого для UNIX и Интернета. В этом случае первое имя и адрес нужно использовать при соеди- нении с помощью DCOM, а второе — при соединении с помощью TCP/IP. Что- бы узнать, каковы в данной сети адреса UNIX и Интернета, следует использо- вать утилиты IPCONFIG и NSLOOKUP.
Удаленный доступ с помощью протокола TCP/IP 507 Безопасность передаваемых данных при работе с компонентом TSocketConnection Один из аргументов против компонента TSocketConnection заключается в том, что при его обычном использовании не обеспечивается безопасность передаваемых данных. Действительно, к данным, передаваемым по протоколу TCP/IP, могут получать доступ злоумышленники. Формат данных также легко может быть рас- шифрован: модуль SConnect.pas, входящий в комплект поставки Delphi, содержит открытый код класса TDataBlocklnterpreter, в котором происходит формирование данных для компонента TCI 1 entDataSet, используемого в ряде DataSnap-серверов (см. главу 12). Однако в Delphi предусмотрена возможность защиты передаваемых пакетов данных от несанкционированного доступа. Механизм защиты очень простой: соз- дается СОМ-сервер, который должен поддерживать интерфейс IDataIntercept. Далее CLSID фабрики классов этого СОМ-сервера указывается в свойстве TSocketConnection. InterceptGUID клиентского приложения, и тот же идентификатор должен быть указан в элементе управления InterceptGUID приложения Scktsrvr.exe, запущенного на сервере. Интерфейс IDataIntercept объявлен в модуле SConnect.pas следующим образом: IDataIntercept = interface ['{B249776B-E429-11D1-AAA4-00C04FA35CFA}' ] procedure Data In(const Data: IDataBlock): stdcall: procedure DataOut(const Data: IDataBlock): stdcall; end: Перед отправкой пакета клиенту (или серверу) серверное (или, соответст- венно, клиентское) приложение обращается к COM-серверу, получает ссылку на интерфейс IDataIntercept и вызывает метод DataOut. Сразу после получения пакета вызывается метод Detain. В качестве параметров обоих методов фигурирует ссылка на интерфейс IDataBlock, который определен в модуле SConnect.pas сле- дующим образом: IDataBlock = interface!IUnknown) ['{СА6564С2-4683-11D1-88D4-0СА0248Е5091}'] function GetBytesReserved: Integer; stdcall; function GetMemory: Pointer: stdcall; function GetSize: Integer: stdcall: procedure SetSize(Value: Integer); stdcall; function GetStream: TStream: stdcall: function GetSignature: Integer: stdcall; procedure SetSignature(Value: Integer); stdcall; procedure Clear; stdcall: function Write(const Buffer: Count: Integer): Integer; stdcall: function Read(var Buffer; Count: Integer): Integer; stdcal1:
508 Глава 11. Удаленный доступ к серверам автоматизации procedure IgnoreStream: stdcall; function InitData(Data; Pointer; DataLen: Integer; CheckLen: Boolean): Integer: stdcall; property BytesReserved: Integer read GetBytesReserved: property Memory: Pointer read GetMemory; property Signature: Integer read GetSignature write SetSignature: property Size: Integer read GetSize write SetSize; property Stream: TStream read GetStream; end; Интерфейс IDataBlock обеспечивает доступ к оперативной памяти, где хранится исходный пакет данных, и содержит ряд методов, позволяющих модифицировать эти данные. Поэтому идеология защиты данных от несанкционированного досту- па заключается в том, что перед отправкой пакета данные кодируются в методе DataOut, а после получения такого пакета данные декодируются в методе Data In. Степень защиты данных определяется выбранным алгоритмом шифрования и мо- жет быть очень высокой. Создадим COM-сервер, реализующий защиту данных, и оформим его в виде DLL. Для этого выберем команду File ► New ► Other, перейдем на страницу ActiveX репозитария и активизируем значок ActiveX Library. Сохраним проект под именем Datalntr. Далее, при открытом проекте вновь обратимся к странице ActiveX и вы- берем значок COM Object. Этот мастер позволяет, среди прочих, создать СОМ- сервер, который не поддерживает библиотек типов и интерфейсов автоматиза- ции. В появившемся окне мастера снимем флажки Mark Interface Oleautomation и Include Type Library и дадим вновь созданному классу имя Datalnt (рис. 11.15). COM Object WiZdrd Рис. 11.15. Создание СОМ-сервера, не поддерживающего библиотеки типов и автоматизацию
Удаленный доступ с помощью протокола TCP/IP 509 После щелчка на кнопке ОК в полученном модуле прежде всего сошлемся на модуль SConnect в секции uses. Далее вручную в определении класса укажем имя поддерживаемого интерфейса: type TDatalnt = class(TComObject. IData Intercept) Два метода этого интерфейса — Data In и DataOut — следует объявить в секции protected класса TDatalnt. Для этого проще всего скопировать их заголовки из файла SConnect. pas. Теперь следует реализовать оба этих метода. Для этого вначале необходимо получить доступ к пакету. Интерфейс IDataBlock имеет два свойства для доступа к памяти: указатель Memory и поток данных Stream. Воспользуемся свойством Stream для доступа к данным. Однако целиком весь поток данных считывать и переко- дировать нельзя, так как первые 8 байт этого потока являются зарезервирован- ными. Четыре байта отводятся для цифровой подписи — по ее значению ком- поненты, ответственные за передачу данных (TSocketConnectlon, TDataSetProvider и др.), определяют, получили ли они данные в доступном формате. В следующих четырех байтах хранится размер пакета — если при перекодировке размер пакета изменяется, то новый размер необходимо сохранять в этой переменной. Реализа- ция кода метода Data In выглядит следующим образом: procedure TDatalnt.Data In(const Data: IDataBlock): var S: TStream: TempStream: TMemoryStream; N, I: Integer: B: Byte; begin TempStream : = nil; try TempStream := TMemoryStream.Create: N : = Data.Size: TempStream.Size := N; S := Data.Stream; S.Seek(Data.BytesReserved. soFromBeginning): S.Read(TempStream.Memory*, N); // Данные копируются в TempStream Data.Clear: TempStream.Seek(0. soFromBeginning); for I := 1 to N do begin TempStream.Read(B. Si'zeOf(B)): В := not B: Data.Write(B. SizeOf(B)); end; Data.Stream: finally TempStream.Free: end: end:
510 Глава 11. Удаленный доступ к серверам автоматизации Сначала создается временный поток TempStream, куда копируется пакет дан- ных. Для копирования пакета запоминаем ссылку S в потоке данных, а начало потока смещаем на 8 байт вызовом метода Seek. Поскольку рассмотрение крипто- графических алгоритмов выходит за рамки темы данной книги, воспользуемся простейшим алгоритмом — инвертируем все биты в пакете данных. Инверсию можно было бы выполнить и без копирования данных в TempStream, но в общем случае, когда при перекодировке меняется размер пакета, создание временного потока обязательно. И последняя, казалось бы, ненужная команда — обращение к свойству (потоку) Data.Stream — устанавливает текущий указатель потока в на- чало. Код в реализации метода DataOut идентичен — повторная инверсия битов вос- станавливает первоначальный результат. Для тестирования данного приложения прежде всего необходимо зарегистриро- вать сервер Datalntr командой Run ► Register ActiveX server. Если сервер и клиент находятся на разных компьютерах, то на второй компьютер необходимо скопиро- вать файл Datalntr.dll, например, в корневой каталог диска С и вызвать команду: С:\WINNT\SYSTEM32\REGSVR32 С:\DataIntr.dl 1 Далее необходимо скопировать значение константы Class_DataInt, запустить приложение scktsrvr.exe (каталог $DELPHI\Bin) на компьютере с СОМ-сервером, щелкнуть правой кнопкой мыши на значке этого приложения, выбрать в контек- стном меню команду Properties и поместить идентификатор фабрики классов в поле GUID открывшегося диалогового окна (рис. 11.16). .ntercept GUID--.-..—.....'4_“-------------- GUIC f(516406С0-С199-11D5-94D60000E8625C26) Intercept GUID is the GUID for a data interceptor COM object. See help for the TSocketCormection for details. .. ...2.:.................... Applv Рис. 11.16. Ввод идентификатора фабрики классов в приложении Borland Socket Server Обратите внимание, что при реализации этого проекта с самого начала на другом компьютере значение GUID будет другим (этот идентификатор тем и от- личается, что он уникален). На всех клиентских компьютерах, которые будут обращаться к этому серверу с помощью компонента TSocketConnection, свойство InterceptGUID также должно ссылаться на GUID фабрики классов созданного перекодировщика — иначе кли- ент получит сообщение о неправильном формате данных (рис. 11.17). При такой реализации по сети будут пересылаться зашифрованные пакеты данных, степень защиты которых от несанкционированного доступа к содер- жимому может быть очень высокой — это зависит от используемого алгоритма шифрования.
Удаленный доступ с помощью протокола HTTP 511 Рис. 11.17. Свойства InterceptGUID и InterceptName компонента TSocketConnection Прежде чем завершить рассмотрение вопросов доступа к удаленным СОМ- серверам по протоколу TCP/IP, напомним основные достоинства и недостатки этого метода удаленного доступа. К несомненным достоинствам следует отнести возможность создания COM-клиентов, не требующих никаких клиентских час- тей и дополнительных настроек на рабочих станциях помимо поддержки самого протокола TCP/IP, а также отсутствие требований к местоположению клиента и сервера, что делает подобную технологию идеальным решением в случае, если мы не можем делать предположений относительно настроек рабочей станции, на которой будет выполняться подобный клиент, или если процесс реализации этих настроек является высокозатратным с точки зрения сопровождения прило- жения (например, при территориально распределенном предприятии или доступе к серверу через Интернет). Из недостатков применения протокола TCP/IP как технологии удаленного доступа к СОМ-серверам следует отметить необходимость отдельного решения проблем, связанных с безопасностью передачи данных, а так- же проблем, которые могут возникнуть при передаче данных через брандмауэры и ргоху-серверы. Отметим, что можно решить проблему передачи данных через брандмауэры, а также применять защищенные протоколы передачи данных, если вместо про- токола TCP/IP использовать протокол HTTP. Об этом пойдет речь в следую- щем разделе. Удаленный доступ с помощью протокола HTTP Помимо возможности удаленного доступа к серверам, основанного на применении DCOM и протокола TCP/IP, в Delphi, начиная с версии 5, на компонентном уровне реализована возможность применения для этой цели протокола HTTP. Это озна- чает, что при соединении с сервером можно использовать брандмауэры и прото- кол SSL (Secure Sockets Layer — слой защищенных сокетов)1, а также создавать пулы ресурсов (resource pools) с целью их более эффективного использования. Протокол HTTP в качестве средства передачи данных обладает еще одним весьма существенным преимуществом при кластеризации серверов, возможной 1 Протокол, гарантирующий безопасную передачу данных по сети и комбинирующий крипто- графическую систему с открытым ключом и блочное шифрование данных.
512 Глава 11. Удаленный доступ к серверам автоматизации в Windows 2000 it Windows Server 2003. Кластеризация с использованием прото- кола HTTP позволяет обеспечить баланс загрузки серверов доступа к данным и устойчивость к сбоям просто средствами операционной системы без иных спе- циальных средств, обеспечивающих подобную возможность. Применение протокола HTTP для удаленного доступа к COM-серверам осно- вано на том же принципе, что и применение протокола ТС/IP, — на компьютере, содержащем СОМ-сервер, запускается универсальный COM-клиент, способный обращаться к определенным категориям COM-серверов и вызывать их методы, при этом COM-клиент взаимодействует не с сервером, а с универсальным СОМ- клиентом, используя для этой цели протокол HTTP. Такой клиент входит в ком- плект поставки Delphi и реализован в виде ISAPI-библиотеки HTTPSRVR.DLL, выполняющейся в адресном пространстве сервера Microsoft Internet Information Services (IIS), начиная с версии 4.0, или сервера Microsoft Personal Web Server (последний доступен на web-сервере корпорации Microsoft). Эту библиотеку следует помещать в каталог, который предназначен для хранения файлов, испол- няемых с помощью IIS по запросу внешних пользователей (если использовать настройки IIS, заданные по умолчанию, то это каталог lnetpub\Scripts), при этом сам сервер IIS должен быть запущен. Принцип доступа к COM-серверу по протоколу HTTP можно кратко сфор- мулировать так: «Доступ к COM-серверу по протоколу HTTP возможен со всех удаленных компьютеров, имеющих доступ к компьютеру, содержащему этот сер- вер, при условии, что на этом компьютере запущен Internet Information Server (или Personal Web Server), для загрузки доступна библиотека HTTPSRVR.DLL или аналогичный ей универсальный клиент, и настройки этого клиента позволяют осуществлять удаленный доступ к данному серверу». Для создания клиентских приложений, обеспечивающих доступ к СОМ-сер- верам, применяется компонент TWebConnection. Для того чтобы приложения, ис- пользующие этот компонент, были работоспособны, на компьютере, содержащем клиентское приложение, должна присутствовать библиотека WININET.DLL, входя- щая в состав Microsoft Internet Explorer, начиная с версии 3.0, и содержащая утилиты, используемые клиентским приложением. Отметим, однако, что компонент TWebConnection применяется только при соз- дании клиентов для DataSnap-серверов. При использовании этого компонента, как и при использовании компонента TSocketConnecti on, можно указать в реестре Windows, доступен ли конкретный DataSnap-сервер для удаленного доступа по протоколу HTTP. Делается это путем вызова функций EnableWebTransport и DisableWebTransport в процедуре UpdateRegistry DataSnap-сервера. Что касается произвольных серверов автоматизации, особенно созданных другими произво- дителями, нам неизвестны случаи применения этого компонента для организа- ции удаленного доступа. Для доступа к COM-серверу с помощью компонента TWebConnection следует установить его свойство ServerName равным ProgID СОМ-сервера, а свойство URL — равным URL компьютера, на котором расположен сервер, например: http://MyHost/scripts/httpsrvr.dll
Применение брокеров 513 Прежде чем завершить рассмотрение вопросов доступа к удаленным СОМ- серверам по протоколу HTTP, напомним основные достоинства и недостатки этого метода удаленного доступа. К достоинствам следует отнести, как и в слу- чае протокола TCP/IP, возможность создания COM-клиентов, не требующих никаких клиентских частей и дополнительных настроек на рабочих станциях, помимо библиотеки WININET.DLL (которая входит в состав всех последних вер- сий Windows), а также отсутствие требований к местоположению клиента и сер- вера. В отличие от протокола TCP/IP протокол HTTP можно применять в сетях с proxy-серверами и брандмауэрами, а также использовать для обмена данными защищенные протоколы. Отметим, однако, что в этом случае компьютер с серве- ром автоматизации, к которому требуется удаленный доступ, придется оснащать постоянно действующим web-сервером. В заключение отметим, что в состав Windows 2000 входит служба СОМ Internet Services Proxy, также предназначенная для того, чтобы обеспечить воз- можность обращения к СОМ-серверам с помощью Internet Information Services и протокола HTTP. Однако обсуждение вопросов применения этой службы вы- ходит за рамки темы данной книги. Применение брокеров Наиболее важными из требований, предъявляемых к распределенным приложе- ниям, являются масштабируемость и доступность. Доступность означает, что клиентские запросы должны обрабатываться в любое время, когда в этом воз- никнет потребность, а масштабируемость — что при увеличении числа клиентов и пропорциональном увеличении числа выделенных для их обслуживания ресур- сов среднее время обработки клиентского запроса не увеличится. Один из способов достижения масштабируемости — применение нескольких однотипных COM-серверов и распределение клиентов между ними с целью достижения баланса их загрузки. Если же при этом организовать переключение клиента на другой сервер в случае отказа того сервера, который его прежде обслуживал, можно соблюсти и требование доступности. Для реализации описанной выше архитектуры распределенных приложе- ний обычно используются средства, которые иногда называются брокерами, иногда агентами (название зависит от конкретной реализации). Назначение этих средств — найти для каждого клиента экземпляр сервера, с которым он будет взаимодействовать (рис. 11.18). Подобный сервис может быть реализован как отдельное приложение (таким образом, например, устроен брокер Business Object Broker, входящий в состав упоминавшегося выше продукта OLEnterprise) или встроен в клиентское приложение (это наиболее простой способ реализации таких сервисов). Для встраивания брокера в клиентское приложение используется компонент TSimpleObjectBroker. Он содержит список компьютеров, на которых можно запус- кать выбранный СОМ-сервер.
514 Глава 11. Удаленный доступ к серверам автоматизации Серверы Рис. 11.18. Распределенное приложение с применением брокеров При попытке обращения клиента к серверу компонент TSimpleObjectBroker выбирает один из серверов в списке (как правило, по очереди). В результате клиенты оказываются более или менее равномерно распределенными между всеми имеющимися серверами, что позволяет достичь баланса загрузки серве- ров. Если при этом реализовать периодическое соединение с сервером в коде клиентского приложения, клиенты смогут при сбое обслуживающих их серверов динамически подключаться к другим серверам на этапе выполнения. При этом следует реализовать и сервер, и клиентское приложение таким образом, чтобы сервер не хранил данные клиента. Для иллюстрации техники применения этого компонента модифицируем наш предыдущий пример приложения, использовавшего компонент TSocketConnecti on. Поместим на форму этого приложения компонент TSimpleObjectBroker и устано- вим свойство Obj ectBroker компонента TSocketConnecti on равным его имени. Затем заполним коллекцию Servers компонента SimpleObjectBrokerl списком имен компь- ютеров (рис. 11.19), на которых установлен и зарегистрирован данный СОМ-сер- вер (в нашем случае — Microsoft Excel). Рис. 11.19. Коллекция Servers компонента TSimpleObjectBroker
Заключение 515 Модифицируем обработчик события OnClick, связанного со щелчком на кнопке: procedure TForml.ButtonlClIck(Sender: TObject): var App: Variant: I: Integer; begin try SocketConnectionl.Connected := True; App := SocketConnectionl.AppServer; App.Visible := True: App.Workbooks.Add: // и так далее... except ShowMessage('Failed to connect to any of servers'): end: end; В блоке try...except компонент TSimpleObjectBroker перебирает коллекцию Servers и пытается запустить сервер на одном из компьютеров до тех пор, пока попытка не окажется удачной. Способ перебора коллекции зависит от значения свойства LoadBalanced компонента TSimpleObjectBroker — если оно равно True, сервер, к ко- торому производится попытка обратиться в первую очередь, выбирается случай- ным образом, если False — он будет первым в списке серверов (в этом случае реализуется другая архитектура распределенного приложения — один главный сервер и несколько резервных). Код в блоке except...end будет выполнен при на- личии исключения, в частности, если ни один из серверов не удастся запустить (для проверки этого кода можно остановить Socket Server на всех компьютерах). Отметим, что аналогичные приложения можно создать и для удаленного дос- тупа с помощью сервисов DCOM и для удаленного доступа с помощью протокола HTTP — все три соответствующих компонента обладают свойством ObjectBroker. Таким образом, с помощью компонента TSimpleObjectBroker можно реализо- вать распределенную систему с COM-серверами, удовлетворяющую требованиям масштабируемости и доступности. Заключение В этой главе мы обсудили вопросы удаленного доступа к серверам автоматиза- ции. Мы узнали, что удаленный доступ удобен, когда сервер требует для своей работы особых ресурсов, недоступных на клиентских компьютерах, или когда он выгоден с точки зрения схемы лицензирования сервера. Мы также узнали, что в общем случае имеется два способа предоставления удаленного доступа к СОМ- серверам: Ж применение сервисов DCOM (Distributed СОМ), когда вся работа по поиску и удаленному запуску приложения возлагается на сервисы операционной системы;
516 Глава 11. Удаленный доступ к серверам автоматизации « применение универсальных COM-клиентов, осуществляющих запуск СОМ- серверов и вызов их методов, а с другой стороны, взаимодействующих с кли- ентом по протоколу TCP/IP или HTTP. Мы обсудили применение сервисов DCOM для реализации удаленного дос- тупа и изучили вопросы предоставления прав на удаленный запуск конкретного приложения тем или иным пользователям с помощью утилиты DCOMCNFG, а также вопросы использования компонента TDCOMConnectl on в клиентских прило- жениях. Обсудив достоинства и недостатки применения DCOM для организа- ции удаленного доступа к СОМ-серверам, мы установили, что: Ж несомненным достоинством является то, что сервисы DCOM интегрированы в операционную систему Windows NT/2000/XP и не требуют установки и за- пуска каких-либо утилит для своего функционирования; » к недостаткам можно отнести то, что компьютер, на котором запускается сер- вер, должен иметь доступ к спискам всех пользователей сети и что сервер ав- томатизации должен быть зарегистрирован в реестре клиента. Мы обсудили применение протокола TCP/IP для удаленного доступа к сер- верам автоматизации. Мы узнали, что: И удаленный доступ к СОМ-серверам по протоколу TCP/IP обычно реализуется путем создания СОМ-клиента, который располагается на том же компьютере, что и сервер, и может инициировать его запуск и вызывать его методы; Ж в состав Delphi входит приложение Borland Socket Server, являющееся таким клиентом. Мы обсудили настройки Borland Socket Server, вопросы создания клиентских приложений с компонентом TSocketConnection и вопросы безопасности передачи данных с помощью компонента TSocketConnection. Мы также познакомились с соз- данием приложений, кодирующих и декодирующих данные, передаваемые с по- мощью Socket Server. Обсудив достоинства и недостатки применения протокола TCP/IP для орга- низации удаленного доступа к СОМ-серверам, мы установили, что: в к достоинствам следует отнести возможность создания COM-клиентов, не требующих никаких клиентских частей и дополнительных настроек на рабо- чих станциях; Ж к недостаткам следует отнести необходимость отдельного решения вопросов, связанных с безопасностью передачи данных, а также возможные проблемы при передаче данных через брандмауэры и ргоху-серверы. Мы обсудили применение протокола HTTP для удаленного доступа к серве- рам автоматизации. Мы узнали, что: Ж удаленный доступ к СОМ-серверам по протоколу HTTP обычно реализуется путем создания СОМ-клиента, который располагается на том же компьютере, что и сервер, и может инициировать его запуск и вызывать его методы; Ж в состав Delphi входит ISAPI-библиотека HTTPSRVR.DLL, выполняющаяся в адресном пространстве Microsoft Internet Information Server и являющаяся COM-клиентом;
Заключение 517 в для создания клиентских приложений, обеспечивающих доступ к СОМ-сер- верам, применяется компонент TWebConnection, однако этот компонент исполь- зуется только при создании клиентов для DataSnap-серверов. Мы обсудили вопросы применения брокеров для создания распределенных приложений, обладающих масштабируемостью и доступностью, а также позна- комились с использованием компонента TSimpleObjectBroker для создания подоб- ных приложений. В этой главе мы часто ссылались на DataSnap-серверы, представляющие со- бой COM-серверы доступа к данным. Технология DataSnap, носившая ранее на- звание MIDAS, появилась впервые в Delphi 3 и сразу же приобрела популяр- ность среди разработчиков. Настало время поговорить об этой технологии более подробно. Именно это мы и сделаем в следующей главе.
ГЛАВА 12 Технология DataSnap Данная глава посвящена созданию многозвенных информационных систем с ис- пользованием технологии DataSnap. В предыдущих версиях Delphi эта техно- логия носила название MIDAS (Multi-tier Distributed Application Service Suite). Технология DataSnap предназначена для создания с помощью Delphi и последую- щей эксплуатации удаленных серверов автоматизации, предоставляющих своим контроллерам доступ к данным серверных СУБД (в данном случае они обычно называются серверами доступа к данным). Информационные системы Информационные службы компаний и предприятий (как крупных, так и неболь- ших) обычно предоставляют (или должны предоставлять в идеале) набор серви- сов, доступных с рабочих мест сотрудников этого предприятия. В общем случае понятие сервиса отнюдь не ограничивается информационной системой компании, предоставляющей сотрудникам доступ к корпоративным данным. Сервисом может быть и доступ к тем или иным файлам, хранящимся в локальной сети, и работа с электронной почтой, и доступ в Интернет, и использование сетевого принтера или модема, и проведение каких-либо расчетов. Доступность того или иного сер- виса в сети нередко определяется теми стандартами, которые он поддерживает (имеются в виду стандартные программные интерфейсы и стандартные протоколы обмена данными). Состав Если рассматривать многопользовательскую работу с корпоративными данными в сети, основанную на применении какой-либо СУБД (в данном случае неважно, сетевой или серверной), можно заметить, что она подчиняется некоторым общим правилам и состоит, как правило, из стандартного набора программных компо- нентов и сервисов. Наиболее важным из таких компонентов является собственно база данных, то есть набор файлов, содержащих данные компании. Этот набор файлов может об- служиваться сервисом, предоставляемым сервером баз данных, если СУБД сер- верная, или файловыми сервисами операционной системы того компьютера, на котором эти файлы расположены, если СУБД не является серверной. Следующим важным компонентом такой системы является набор пользо- вательских приложений, служащих для редактирования и просмотра данных на
Информационные системы 519 рабочих станциях сотрудников. В этом случае говорят, что такие приложения содержат презентационную логику информационной системы. Нередко пользо- вательские приложения применяются для других операций с данными (проверка значимости данных, статистическая обработка, генерация отчетов и др.). В этом случае говорят о том, что такое приложение содержит алгоритмы прикладной обработки данных. Немаловажной составляющей частью любой информационной системы явля- ется набор сервисов, обеспечивающих бизнес-логику ее функционирования, таких как обработка данных, проведение расчетов, генерация отчетов и др. В составе информационной системы могут быть и иные немаловажные сервисы, например сервисы управления каким-либо оборудованием. Еще один компонент, без которого работа сетевой информационной системы невозможна, — это набор средств обеспечения доступности данных из СУБД в пользовательском приложении. Он существенно зависит от того, является ли СУБД серверной. Как минимум, во всех случаях это средства сетевого доступа, базирующиеся на сетевых средствах операционных систем, применяемых для эксплуатации СУБД и пользовательских приложений. Сетевые средства опера- ционных систем включают, как минимум, поддержку сетевых протоколов, обес- печивающих этот доступ. В случае серверных СУБД к этому набору добавляются средства взаимодей- ствия пользовательского приложения и сервера баз данных, использующие ту же самую поддержку сетевых протоколов операционными системами. Эти сред- ства обычно включают клиентскую часть серверной СУБД, содержащую, как пра- вило, низкоуровневый интерфейс для взаимодействия с сервером баз данных. Помимо этого, средства обеспечения доступности данных нередко содержат биб- лиотеки, реализующие какой-нибудь универсальный механизм доступа к данным. Эти функции упрощают использование клиентской части, если СУБД сервер- ная, либо реализуют стандартные операции с данными, если СУБД не является серверной. В случае пользовательских приложений, созданных с помощью средств разработки Borland, это библиотека Borland Database Engine (BDE) с драйверами SQL Links, библиотеки dbExpress, а также библиотеки ADO (ActiveX Data Objects), ODBC-драйверы и OLE DB-провайдеры. Типичные проблемы Классические многопользовательские системы, как правило, содержат на рабо- чих станциях приложения, содержащие презентационную логику, а также сред- ства доступа к данным. Из этого следует, что такие рабочие станции должны предоставлять для самих себя весь необходимый им набор сервисов и содер- жать соответствующее программное обеспечение для их функционирования. Это нередко усложняет технические требования, предъявляемые к аппаратной час- ти клиентской рабочей станции, и в конечном итоге приводит к повышению стоимости эксплуатации и сопровождения такой информационной системы (рис. 12.1).
520 Глава 12. Технология DataSnap Весьма существенным является то, что значительная часть бизнес-логики таких информационных систем содержится в клиентских приложениях, что повышает требования, предъявляемые к рабочим станциям. Естественно, некоторая часть такой логики может быть возложена на сервер, но возможности серверных СУБД в этом отношении весьма ограничены. Рис. 12.1. Классическая информационная система Следует отметить, что подобное программное обеспечение обычно требует настроек и поддержания этих настроек в рабочем состоянии. Так, пользователь- ское приложение должно как минимум «знать» о том, где расположены исполь- зуемые им данные, какого они типа (имеется в виду тип серверной СУБД либо формат данных сетевой СУБД), с помощью какого сетевого протокола они доступны, каков поддерживаемый базой данных язык, определяющий порядок алфавитной сортировки и индексирования данных, каковы соответствующие настройки библиотек, реализующих универсальный механизм доступа к данным и клиентской части серверной СУБД. Подобная работа нередко является весьма трудоемким процессом, особенно при большом количестве и неоднородном парке рабочих станций. Отметим, что далеко не все компоненты подобного программ- ного обеспечения могут быть включены в состав дистрибутива пользователь- ского приложения, так как многие из них являются предметом лицензирования и продажи.
Информационные системы 521 Есть и еще один немаловажный фактор: чем сложнее конфигурация, обеспе- чивающая доступ к данным рабочей станции, тем чаще происходят нарушения в ее работе. По данным некоторых западных источников, повторное конфигури- рование и сопровождение программного обеспечения, предоставляющего доступ рабочих станций к данным, приводит в среднем к четырем дням простоя рабочей станции в год. Следующий фактор напрямую связан с тем, что многие средства разработки используют одни и те же стандартные библиотеки доступа к данным (в случае Windows это ADO, ODBC, иногда BDE). На сегодняшний день как на российском, так и на мировом рынке имеется немалое количество различных программных продуктов, содержащих какие-либо данные (в особенности энциклопедий и спра- вочников), при установке которых устанавливаются и эти библиотеки (подобные действия разрешены при определенных условиях производителями этих библио- тек). В этом случае, особенно при применении библиотек BDE, нет стопроцент- ной гарантии, что версия любой из подобных библиотек, входящая в комплект поставки такого рода продукта, окажется новее, чем установленная в корпоратив- ной информационной системе, и что программа установки «чужого» продукта не внесет изменений в настройки, сопровождающие эту библиотеку, таким образом, что работоспособность вашей, уже установленной, информационной системы окажется нарушенной. Подобная ситуация, конечно, противоречит правилам создания дистрибутивов, но такие случаи время от времени случаются даже с не- плохими коммерческими продуктами, при этом далеко не во всех организациях действуют жесткие ограничения на применение тех или иных программных про- дуктов на рабочих местах пользователей. Итак, используя стандартные архитектуры создания многопользовательских информационных систем, можно столкнуться с серьезными проблемами, требую- щими материальных затрат, — все более повышающимися требованиями к аппарат- ному обеспечению рабочих станций и необходимостью поддерживать в актуальном состоянии настройки доступа к данным. Отметим, что список возможных проблем этим не исчерпывается. Мы не рассматривали проблемы, связанные с перегруз- кой сети при росте объемов передаваемых данных (частично они могут быть ре- шены заменой сетевых СУБД серверными, хотя далеко не всегда такой переход полностью решает проблемы), с эксплуатацией всеми пользователями каких-либо общих ресурсов (например, высококачественного многопроцессорного сервера, способного обрабатывать данные гораздо быстрее пользовательских рабочих станций), а также проблемы, возникающие из-за территориальной разбросанно- сти предприятия или низкого качества линий связи. Способы решения проблем Каким образом можно решить весь спектр проблем? Способов решения доста- точно. Нередко применяются так называемые экстенсивные меры (наращивание аппаратной части рабочих станций, увеличение пропускной способности сети, прокладка новых линий связи, перенос прошлогодних данных в архивы с целью уменьшения объема базы данных), которые обычно требуют немалых материалы
522 Глава 12. Технология DataSnap ных затрат, особенно при большом количестве пользователей и высоких темпах роста объема базы данных. Есть, однако, и иные, интенсивные способы решения подобных проблем. Эти способы могут быть по-разному реализованы, но идея у них одна — она заключа- ется в создании новых сервисов, общих для пользователей информационной системы. Такие сервисы, как правило, являются сервисами промежуточного звена (middle- ware services), поскольку занимают промежуточный слой между данными и серви- сами, их обслуживающими, с одной стороны, и пользовательскими приложения- ми, ориентированными на конкретную предметную область, с другой стороны. Эти сервисы обычно обладают минимальным пользовательским интерфейсом или не имеют его вовсе. Нередко они могут быть реализованы для нескольких платформ, так как являются более высокоуровневыми сервисами, чем сервисы, специфичные для какой-то операционной системы или СУБД. Такие сервисы могут быть реализованы внутри приложений или библиотек, а также в виде служб операционных систем. Технологии, используемые для реализации таких сервисов, могут быть раз- личными, и в общем случае набор возможных клиентских и серверных платформ может быть весьма широк и отнюдь не ограничиваться различными версиями Windows. Если же речь идет об относительно недорогих решениях на основе Windows, для создания таких сервисов удобно использовать технологию DCOM или различные расширения СОМ (например, технологию Borland DataSnap) и реализовывать сервисы промежуточного слоя внутри серверов автоматизации или компонентов Microsoft Component Services. Введение в технологию DataSnap DataSnap представляет собой технологию создания распределенных систем, со- стоящих из сервера баз данных, сервера доступа к данным (который, в свою оче- редь, является клиентом сервера баз данных) и так называемого тонкого, или облегченного, клиентского приложения, являющегося клиентом сервера доступа к данным (рис. 12.2). Фактически два последних приложения делят между собой функциональность, характерную для клиентского приложения, используемого в «классических» двухзвенных клиент-серверных системах. Тонкий клиент обычно является при- ложением, с которым работает конечный пользователь, и поэтому предназначен главным образом для предоставления пользовательского интерфейса (то есть тех форм и интерфейсных элементов, с помощью которых пользователь редактирует данные). Естественно, такое приложение должно «знать», на каком компьютере локальной или глобальной сети находится сервер доступа к данным, каково имя (или иной идентификатор) предоставляемого им сервиса и с помощью каких средств (имеются в виду сервисы операционной системы, сетевые протоколы и т. д.) с ним можно обмениваться этими данными. Это и есть те немногочислен- ные параметры, которые требуют настройки.
Введение в технологию DataSnap 523 Рис. 12.2. Информационная система с сервером доступа к данным Что касается сервера доступа к данным, обычно он конечным пользователям недоступен, и поэтому пользовательский интерфейс в традиционном понимании (формы, кнопки, поля для ввода данных) иметь может, по не обязан. Иными словами, сервер доступа к данным может быть и обычным Windows-приложением с формами, и приложением без форм, и консольным приложением, и даже просто сервисом операционной системы, пишущим сообщения для администратора системы в файл журнала (log file). Его задача — обмениваться данными с тон- ким клиентом и обращаться к серверу баз данных с собственными запросами (обычно инициированными этим обменом). Поэтому сервер доступа к данным, с одной стороны, должен предоставлять клиентам интерфейсы, позволяющие по- лучать от него данные, а с другой стороны, быть полноценным клиентом сервера баз данных. Иными словами, содержащий его компьютер должен иметь как
524 Глава 12. Технология DataSnap минимум установленную клиентскую часть серверной СУБД. Нередко такой компьютер имеет и другие библиотеки доступа к данным. Например, в версиях MIDAS 1 и MIDAS 2 (Delphi 3 и Delphi 4) обязательной составляющей его частью была библиотека Borland Database Engine. В версии MIDAS 3 (Delphi 5) и более поздних в качестве механизма доступа к данным могут быть использо- ваны и другие библиотеки, например библиотеки ADO (или вообще никаких библиотек, кроме тех, которые поддерживают клиентский API и поставляются с сервером баз данных). И наконец, с выходом Delphi 6 к разнообразным меха- низмам доступа к данным, применяемым в технологии DataSnap, добавился но- вый универсальный механизм — dbExpress, который получил дальнейшее разви- тие в Delphi 7. DataSnap-сервер доступа к данным представляет собой СОМ-сервер (обсужде- ние вопросов применения совместно с технологией DataSnap технологии CORBA и иных технологий распределенных вычислений, не базирующихся на модели СОМ, выходит за рамки темы данной книги). С технологической точки зрения DataSnap есть реализованная в ряде компонентов VCL надстройка над СОМ, осуществляющая превращение набора данных в тип, допустимый для СОМ, пе- редачу таких данных обычным для СОМ способом и обратное восстановление набора данных на стороне, эти данные получающей. Лицензионная точка зрения на DataSnap практически совпадает с технологи- ческой — в случае Delphi версий 4-6 оплате подлежит возможность передавать наборы данных с одного компьютера на другой; существенная разница заключа- ется лишь в том, что в пределах одного компьютера данные можно передавать, не покупая лицензий. Отметим, что поставка распределенных DataSnap-прило- жений, разработанных с помощью Delphi 7 Studio, может осуществляться без до- полнительного лицензирования. Для создания DataSnap-серверов используются имеющиеся в Delphi компо- ненты доступа к данным (наследники класса TDataSet) и серверные DataSnap- компоненты, предоставляющие клиентскому приложению данные, полученные с помощью компонентов доступа к данным, такие как TDataSetProvider (а в ран- них версиях MIDAS — TProvider). Клиентские DataSnap-приложения, в свою очередь, используют ряд компонентов, отвечающих за обмен данных с сервером (к ним относятся рассмотренные в предыдущей главе компоненты TDCOMConnection, TSocketConnection, TWebConnection), и компонент TCHentDataSet, осуществляющий кэширование полученных данных. Справедливости ради надо отметить, что никто не запрещает создавать соб- ственную реализацию преобразования набора данных в тип, воспринимаемый СОМ, и аналогичного обратного преобразования. Такая реализация может стать альтернативой технологии DataSnap. В настоящее время на рынке компонентов имеется несколько подобных реализаций. Кроме того, некоторые разработчики применяют для сходных целей технологию Remote Data Services (RDS), являю- щуюся составной частью ADO (рассмотрение этой технологии выходит за рамки темы данной книги, интересующиеся RDS могут обратиться к соответствую- щему разделу MSDN или к книге «Advanced Delphi Developers Guide to ADO», A. Fedorov and N. Elmanova, Wordware Publishing, 2000).
Создание простейшего DataSnap-приложения 525 Когда следует выбирать DataSnap в качестве технологии распределенных вы- числений? Делать это следует в том случае, когда: серверы доступа к данным планируется эксплуатировать под управлением различных версий Windows без применения других платформ; может потребоваться создавать клиентские приложения, не требующие кон- фигурирования; число клиентских приложений может оказаться либо большим, либо непред- сказуемым. Далее мы расскажем более подробно, как применять эту технологию, а затем обсудим некоторые особенности, связанные с переносом в Delphi 7 проектов, разработанных для ранних версий MIDAS. Создание простейшего DataSnap-приложения Изучение технологии DataSnap резонно начать с примеров простейших прило- жений — именно такое приложение будет создано в данном разделе. Создание сервера Сервером баз данных в проектах будет Microsoft SQL Server. Доступ к используе- мой в наших примерах базе данных Northwind, которая входит в комплект по- ставки этой СУБД, будем осуществлять с помощью ADO. Сначала разработаем сервер доступа к данным. Для этого создадим новое приложение, выбрав команду File ► New application. Это приложение будет за- пускаться по требованию клиента. Далее выберем команду File ► New ► Other, перейдем на страницу Multitier репозитария и активизируем значок Remote data module. В результате появится диалоговое окно, похожее на то, которое мы запол- няли при создании серверов автоматизации (см. главу 3), но без флажка Generate Event Support Code (возможность создать описание интерфейса нотификаций у DataSnap-серверов отсутствует). Дадим нашему COM-классу имя Test, в рас- крывающемся списке Instancing выберем пункт Multiple Instance, а в раскрываю- щемся списке Threading Model — пункт Apartment. Смысл этих параметров уже неоднократно объяснялся (см. главы 3 и 6). После этого будет создан модуль данных, в который, как и в обычный модуль данных, можно помещать невизу- альные компоненты. Сохраним проект под именем MidServ. Поместим в созданный модуль данных компонент TADOConnection и определим его свойство Connect! onStri ng. Для этого в инспекторе объектов щелкнем на кнопке с многоточием в строке этого свойства. В качестве провайдера данных в списке открывшегося окна выберем пункт Microsoft OLE DB provider for SQL server, щелкнем на кнопке Next и заполним вкладку Connection диалогового окна свойств (рис 12.3).
526 Глава 12. Технология DataSnap Рис. 12.3. Задание параметров соединения с Microsoft SQL Server При задании параметров доступа к базе данных следует указать имя SQL- сервера (обратите внимание на то, что в вашем случае оно, скорее всего, будет отличаться от показанного на рисунке имени TREPA) и обязательно ввести дан- ные, необходимые для аутентификации пользователя. Если пароль пользователя не требуется (как в данном случае), следует установить флажок Blank password. Обязательно следует установить флажок Allow saving password, поскольку в сер- верном приложении аутентификация пользователей не нужна (напомним, что пользователи не имеют непосредственного доступа к клиентскому приложению, и на запрос о вводе имени пользователя и пароля ответить будет некому). Аутен- тификацию пользователя в случае необходимости можно будет в дальнейшем реализовать в клиентском приложении. И наконец, следует выбрать на сервере базу данных Northwind. Если все выполнено правильно, то при щелчке в этом диалоговом окне на кнопке Test Connection должно появиться сообщение об успешном соединении. Свойство LoginPrompt компонента ADOConnectionl установим равным False. Если этого не сделать, при обращении клиента к серверу приложений диалоговое окно Login, предназначенное для ввода имени и пароля пользователя, будет появляться на компьютере, где установлен сервер доступа к данным, но, как мы только что заметили, этого допускать не следует. Чтобы проверить правильность всех параметров, следует установить свойство Connected компонента ADOConnectionl равным True. При этом не должно появляться
Создание простейшего DataSnap-приложения 527 диалогового окна Login, не должно возбуждаться исключений, а значение этого свойства в окне инспектора объектов должно визуально измениться. Поместим в модуль данных компонент TADOTable. В свойстве Connection этого компонента сошлемся на компонент ADOConnectionl. Свойство ТаЫ eName устано- вим равным Clients. И наконец, поместим в модуль данных компонент TDataSetProviden (рис. 12.4) и в его свойстве DataSource сошлемся на компонент ADOTablel. Рис. 12.4. Создание простейшего сервера доступа к данным На этом создание простейшего сервера доступа к данным можно считать за- конченным. Его необходимо скомпилировать и один раз запустить с целью реги- страции в системном реестре. Прежде чем приступить к созданию клиентского приложения, посвятим не- сколько строк вопросам создания сервера приложений с доступом к данным с по- мощью механизма BDE. В первую очередь необходимо поместить в удаленный модуль данных компонент TSession и установить его свойство AutoSessi onName равным True, чтобы не было конфликтов между соединениями с базой данных, устанавливаемых в разных экземплярах модуля данных. Если этого не сделать, то многочисленные клиенты будут работать в рамках одного сеанса, и при возбу- ждении исключения у кого-нибудь из них это отразится на других клиентах. Для того чтобы избавиться от диалогового окна Login, следует использовать компонент TDatabase. В его свойстве AliasName необходимо сослаться на существую- щий псевдоним (alias) BDE и ввести уникальное имя базы в свойстве DatabaseName. В дальнейшей разработке во всех компонентах доступа к данным (TQuery, TTable, TStoredProc) в свойстве DatabaseName необходимо ссылаться только на это имя. Чтобы диалоговое окно Login не появлялось при работе сервера доступа к данным, необходимо заполнить свойство Params компонента TDatabase — ниже показано, как выглядят значения этого свойства для базы данных EMPLOYEE.GDB, входящей в комплект поставки IB Database: USERNAME=SYSDBA PASSWORD=masterkey Обратите внимание на отсутствие пробелов после знака равенства (=). Иначе такой пробел был бы добавлен в имя пользователя или пароль и, соответственно, привел бы к исключению при аутентификации пользователя.
528 Глава 12. Технология DataSnap И, наконец, свойство LoginPromt компонента TDatabase необходимо установить равным False. Тестируется правильность параметров так же, как и в случае ком- понента TADOConnection: при попытке установить свойство Connected равным True не должно появляться диалогового окна Login и возбуждаться исключений. ВНИМАНИЕ --------------------------------------------------------------- Применение механизма BDE во вновь создаваемых проектах категорически не реко- мендуется самой компанией Borland, поэтому задействовать этот механизм, равно как и изложенные выше рекомендации, следует только в случае каких-либо безвыход- ных ситуаций (например, необходимости применения очень старых версий каких- либо СУБД, для доступа к которым может оказаться проблематичным использование dbExpress или ADO). Создание клиента Создав сервер, мы можем приступить к созданию клиентского приложения. После выбора команды File ► New application, прежде всего, необходимо выбрать способ передачи данных — от способа передачи зависит, какой компонент TXXXConnection потребуется. Возможные способы передачи данных детально разбирались в преды- дущей главе; в данном проекте мы просто выберем компонент TSocketConnecti on. Это означает, что в качестве сервиса, который будет запускать сервер приложе- ний, будет использоваться Borland Socket Server. Поэтому на компьютере, где на- ходится созданный в предыдущем разделе сервер доступа к данным, необходимо запустить приложение scktsrvr.exe (оно находится в каталоге Delphi7\Bin). Можно сразу же зарегистрировать это приложение как сервис Windows NT/2000, вызвав его с помощью следующей команды: C:\Program Files\Borland\Delphi7\Bin\ScktSrvr.exe /install Поместим на форму клиентского приложения компонент TSocketConnecti on со страницы DataSnap палитры компонентов и в свойстве Address определим 1Р-ад- рес компьютера, где находится созданный ранее сервер приложений (это может быть как тот же самый компьютер, на котором разрабатывается клиент, так и ка- кой-нибудь другой, доступный с помощью протокола TCP/IP). Можно также определить имя компьютера в свойстве Host компонента TSocketConnecti on. Далее в свойстве ServerName укажем имя сервера доступа к данным. Для про- екта, созданного в предыдущем разделе, это будет MidServ.Test (MidServ — имя ис- полняемого файла, Test — имя класса удаленного модуля данных). Если сервер зарегистрирован на компьютере, где разрабатывается клиент, то его имя можно выбрать в раскрывающемся списке. В любом случае при этом сразу же окажется заданным значение свойства ServerGUID. При разработке подобного проекта в Delphi 4 (вследствие ошибки этой версии) для запуска сервера на удаленном компьютере необходимо было заполнить только свойство ServerGUID; свойство ServerName требовалось оставить пустым. На этом этапе для тестирования правильности ввода данных, регистрации сервера доступа к данным и запуска сервиса scktsrvr.exe можно попытаться устано- вить свойство Connected компонента TSocketConnecti on равным True. При правильных действиях это значение изменится и запустится сервер доступа к данным. Если
Создание простейшего DataSnap-приложения 529 происходит исключение, надо установить его причину, в противном случае не- возможно будет выполнить дальнейшие действия. Теперь поместим на форму компонент TClientDataSet. В DataSnap-приложе- ниях он выполняет функции набора данных в клиентском приложении. При этом неважно, с помощью каких компонентов доступа к данным (ТТаЫе, TADOTable, TQuery и т. д.) наборы данных были получены сервером — для любого из них ис- точников данных в клиентском приложении будет компонент TC11 entDataSet. В свойстве Connection компонента Cl 1 entDataSet 1 сошлемся на компонент SocketConnectlonl. После этого можно изменить свойство ProviderName. В раскры- вающемся списке следует выбрать компонент DataSetProviderl (на данном этапе разработки проекта список состоит из одного пункта). При этом произойдет запуск сервера доступа к данным (если таковой еще не запущен), и свойство Connected компонента SocketConnectlonl автоматически установится равным True. Рекомендуется тем не менее снова установить его равным False. Дальнейшие действия не отличаются от тех, что выполняются при созда- нии обычных приложений с базами данных: на форму помещается компонент TDataSource, который связывается с компонентом ClientDataSetl. Далее на форму можно поместить компоненты отображения данных (пусть это будет TDBGrid), связанные с DataSourcel (рис. 12.5). Рис. 12.5. Простейшее клиентское DataSnap-приложение Для дальнейших действий нам понадобится динамическое соединение с сер- вером доступа к данным. Поэтому поместим на форму кнопку и создадим для нее следующий обработчик события OnCHck: procedure TForml.ButtonlClick(Sender: TObject): begin if SocketConnectionl.Connected then begin SocketConnectionl.Connected := False: Buttonl.Caption := 'Connect'; end else begin SocketConnectionl.Connected := True: ClientDataSetl.Active := True:
530 Глава 12. Технология DataSnap Buttonl.Caption := 'Disconnect'; end; end: Кнопка будет работать как переключатель — при наличии соединения с серве- ром это соединение будет разрываться, и, наоборот, при отсутствии — устанав- ливаться. Скомпилировав проект, запустив его на выполнение и щелкнув на кнопке, можно увидеть данные в клиентском приложении (рис. 12.6). Рис. 12.6. Работа DataSnap-сервера и DataSnap-клиента на одном компьютере Данные в компоненте DBGridl можно редактировать. При этом при переходе на следующую запись автоматически выполняется команда Post. Однако после закрытия клиентского приложения и повторного его запуска можно заметить, что все внесенные изменения пропали. Причина этого заключается в том, что все изменения компонент TClientDataSet запоминает в памяти компьютера — изме- нения кэшируются. Для того чтобы передать их серверу, необходимо выполнить метод Applyllpdates компонента TClientDataSet. Поместим на форму кнопку и соз- дадим следующий обработчик событий: procedure TForml Button2Click(Sender: TObject); begin if not SocketConnectionl.Connected then Exit; if CllentDataSetl.Applyllpdates(-l) = 0 then Cli entDataSetl.Refresh; end; Сначала проверяется наличие соединения с сервером — если его нет, то, по по- нятным причинам, вызов метода Applyllpdates приведет к исключению. Затем вызы- вается метод Applyllpdates. В качестве параметра этот метод принимает максималь- ное количество ошибок, которые могут произойти на сервере при попытке сохранить данные в базе данных. По достижении этого значения метод Applyllpdates прекращает
Модель Briefcase 531 попытки изменения записей на сервере. Значение -1 означает, что все изменен- ные записи будут передаваться серверу независимо от количества ошибок. Этот метод возвращает реальное количество ошибок, имевших место при передаче моди- фицированных данных на сервер. При равенстве нулю этого значения следует вызвать метод Refresh, который позволяет прочесть изменения, внесенные другими пользователями с момента его последнего вызова. Попытка вызвать метод Refresh без вызова метода Applyllpdates при наличии изменений приводит к исключению. Обратите внимание на следующий факт: при повторном щелчке на кнопке Connect происходит отсоединение от сервера доступа к данным, и серверное при- ложение закрывается. Тем не менее данные в компоненте DBGridl сохраняются, их можно просматривать и редактировать. Объяснение этому будет дано в сле- дующем разделе. Модель Briefcase Как уже упоминалось ранее, клиентское приложение работает с локальной копией данных, содержащейся в кэше рабочей станции. Поэтому наличие постоянного соединения с сервером доступа к данным необязательно. Достаточно соединяться с ним периодически с целью обновления отредактированных данных или загрузки данных, введенных другими пользователями. Так как частота соединений ничем не регламентирована, данные можно скопировать на переносной компьютер и от- соединиться от сети. После этого достаточно длительное время данные можно редактировать без соединения с сервером, с помощью метода SaveToFile сохра- нять их в локальном файле, с помощью метода LoadFromFile изымать их из него для продолжения редактирования и обновлять, когда есть доступ к серверу. Такая организация работы пользователей получила название модели briefcase («briefcase» означает «портфель»). Помимо традиционного использования модели briefcase возможны и другие варианты, например выгрузка данных в локальный файл и поставка его вместе с приложением, о чем будет рассказано далее. На созданную ранее форму клиентского приложения поместим две кнопки и присвоим их свойству Caption значения Save и Load. Поместим на форму ком- поненты TSaveDialog и TOpenDialog. Создадим обработчики событий, связанных со щелчками на кнопках: procedure TForml.Button3Click(Sender: TObject): begi n if not Cl ientDataSetl.Active then Exit: if SaveDialogl.Execute then Cl i entDataSet1.SaveToFi1e(SaveDi alogl.Fi1 eName): end: procedure TForml.Button4Click(Sender: TObject): begin if OpenDIalogl.Execute then Cl ientDataSetl.LoadFromFile(OpenDialogl.FileName): end:
532 Глава 12. Технология DataSnap Перед сохранением данных необходимо убедиться, что компонент ClientDataSetl содержит данные — для этого проверяется его свойство Acti ve. Смысл остального кода очевиден. Для тестирования необходимо запустить клиентское приложение и установить связь с сервером щелчком на кнопке Connect. После получения данных следует сохранить их в локальном файле, щелкнув на кнопке Save. Далее нужно закрыть клиентское приложение, затем открыть его снова, не соединяясь с сервером, щелкнуть на кнопке Load и выбрать сохраненный файл. Данные снова станут доступными для редактирования, но при этом можно отметить отсутствие загру- женного экземпляра сервера. Отредактированные данные можно снова сохранять в файле, как в том же самом, так и в другом. Для того чтобы перенести на сервер отредактированные данные, сохраненные в локальном файле, после щелчка на кнопке Load необходимо щелкнуть на кнопке Connect. После успешного соединения с сервером нужно выполнить ме- тод ApplyUpdates, щелкнув на кнопке Update. Модель briefcase особенно удобно применять в тех случаях, когда организа- ция имеет распределенные филиалы, причем постоянная связь между филиалами неосуществима или ненадежна. Однако следует отдавать себе отчет в том, что клиенты не будут знать об изменениях, выполненных другими клиентами, пока данные не окажутся на сервере, и это в той или иной степени характерно для всех систем, основанных на кэшировании данных в клиентских приложениях. Предположим, что два клиента загрузили и начали редактировать один и тот же исходный набор данных. Вряд ли они внесут одинаковые изменения, поэтому при передаче изменений на сервер возникает вопрос: а изменения какого клиента следует сохранять на сервере? Наиболее простой ответ — сохранять последние внесенные изменения. Но очевидно, что при таком подходе не избежать записи ошибочных данных. Как решается эта проблема, можно узнать из следующего раздела. Многопользовательская обработка данных в распределенных системах Рассмотрим часто встречающийся при многопользовательской обработке данных случай, когда два пользователя пытаются отредактировать одну и ту же запись. При работе с сетевыми версиями настольных СУБД и с серверными СУБД в такой ситуации запись (или несколько записей, занимающих общую единицу хранения информации, называемую страницей) обычно блокируется, то есть пользователю запрещается редактировать запись, уже редактируемую другим пользователем, пока последний ее не освободит. В случае трехзвенной системы механизм блокировок традиционной двухзвенной модели «клиент-сервер» может оказаться неприемлемым, так как при использовании модели briefcase промежуток времени между редактированием записи и сохране- нием ее в базе данных может быть весьма длительным. Поэтому организация мно- гопользовательской работы в трехзвенных системах отличается от привычной. При попытке сохранения сервером доступа к данным измененной записи в базе данных производится поиск этой записи либо по ключевому полю, либо по всем
Многопользовательская обработка данныхвраспределенных системах 533 полям (в зависимости от значения свойства UpdateMode компонента TDataSet, ответственного за этот процесс на сервере) и сравнение всех полей изменяемой записи с исходными значениями (то есть теми значениями, которые были в кэше клиента на момент получения этой записи с сервера и до того, как пользователь изменил в кэше эту запись). Если какие-либо поля за время между получением оригинала записи клиентом и попыткой сохранить изменения были модифици- рованы другим пользователем, запись может быть передана обратно в клиент- ское приложение для дальнейшей обработки пользователем. Так как данные, предоставляемые компонентом TClientDataSet, — это содер- жимое кэша, вполне возможно, что несколько пользователей создадут свои ло- кальные копии исходных записей, полученных с сервера баз данных, и каждый начнет их редактировать. Предположим, два пользователя отредактировали одну и ту же запись и пытаются сохранить ее на сервере. В этом случае тот из пользо- вателей, кто пытался первым сохранить в базе данных свой вариант отредакти- рованной записи, сумеет это сделать, тогда как второй пользователь должен быть уведомлен об ошибке, связанной с тем, что сохраняемая им запись уже изменена другим пользователем. Для обработки подобных ошибок используется событие OnReconcil eError ком- понента TClientDataSet, возникающее в случае, когда при попытке сохранения из- мененной записи в базе данных выясняется, что запись была изменена другим пользователем. Пример диалогового окна для обработки такой ситуации можно найти в репозитарии объектов, и во многих случаях имеет смысл им воспользо- ваться или изменить его для своих целей. На странице Dialogs окна репозитария объектов выберем значок Reconcile Error Dialog. В результате к проекту будет добавлено диалоговое окно, показанное на рис. 12.7. Сохраним модуль под именем UC1 i 2. Далее включим ссылку на модуль вновь созданного диалогового окна в секцию uses модуля, связанного с главной формой проекта. Если приложение создается в Delphi 3 или 4, то следует также перене- сти вновь созданное окно в список Available Forms на вкладке Forms диалогового окна параметров проекта — в противном случае наше новое окно будет появ- ляться при запуске приложения. Затем создадим обработчик события OnReconci 1 eError компонента Cl i entDataSetl: procedure TForml.Cli entDataSetlReconci 1 eError( DataSet: TCustomClientDataSet: E: EReconci 1 eError: UpdateKind: TUpdateKind; var Action: TReconcileAction): begin Action := Handl eReconcileError(DataSet. UpdateKind. E): end: Скомпилируем и сохраним проект, а затем запустим две копии созданного приложения. В каждой из копий приложения внесем разные изменения в одно и то же поле одной и той же записи и попытаемся сохранить изменения в базе данных. При этом первое изменение с успехом сохранится, а при попытке внести второе изменение появится вновь созданное диалоговое окно (рис. 12.8).
534 Глава 12. Технология DataSnap Рис. 12.7. Диалоговое окно Reconcile Error Dialog из репозитария объектов Рис. 12.8. Сообщение о конфликте, получаемое пользователем при попытке сохранения измененных данных В данном диалоговом окне пользователь может внести свое значение в базу данных, оставить значение, внесенное предыдущим пользователем, или вернуться к первоначальному (неотредактированному) значению. Пользователь выбирает способ обработки ошибки при помощи группы переключателей Reconcile Action: a Skip — измененная запись пропускается и затем возбуждается исключение (записи с пропущенными изменениями сохраняются в файле журнала); S: Cancel — на сервер передаются оригинальные значения, которые существовали до модификаций, выполненных обоими пользователями;
Создание клиентских приложений в виде активных форм 535 И Correct — поля текущей редактируемой записи заменяются значениями, ука- занными пользователем в этом диалоговом окне; ж Refresh — все изменения в текущей записи отменяются и сохраняются значе- ния, которые находятся на сервере; » Merge — измененная запись объединяется с записью на сервере. Вид этого диалогового окна зависит от того, содержит ли таблица первичные ключи. При наличии первичных ключей (как в данном случае) поиск отредакти- рованной записи производится по совпадению первичного ключа. При отсутст- вии первичного ключа поиск отредактированной записи производится по совпа- дению значений всех полей. В этом случае в группе Reconcile Action отсутствуют переключатели Refresh и Merge, а содержимое столбца Conflicting Value не отобра- жается, поскольку сервер не может найти изменения, которые были сделаны предыдущим пользователем. Если по каким-либо причинам обработка коллизий при многопользователь- ской работе должна быть отлична от предложенной, можно отредактировать имеющийся код (и при необходимости изменить само диалоговое окно). Напри- мер, английский текст в этом окне может быть заменен русским. Создание клиентских приложений в виде активных форм Есть еще один способ создания тонкого клиента, в ряде случаев весьма удобный. Он базируется на возможностях активных форм (разработка подобных приложе- ний была описана в главе 2). Создание клиента в виде элемента управления ActiveX Самый простой способ создать элемент управления ActiveX заключается в сле- дующем. На главной форме клиентского приложения, созданного в предыду- щем примере, выделим все интерфейсные элементы и выберем в меню команду Component ► Create Component Template. Откроется диалоговое окно, в котором можно задать параметры создания шаблона компонентов (рис. 12.9). Согласимся с параметрами, предлагаемыми по умолчанию. В результате на странице Templates палитры компонентов появится шаблон TDBGridTemplate (название компонента может отличаться — оно зависит от того, какой компонент был первым помещен на форму клиентского приложения). Теперь можно сохра- нить и закрыть созданную группу проектов. Создадим проект, содержащий активную форму, как это было описано в гла- ве 2. На пустую активную форму поместим только что созданный компонент TDBGridTeniplate. Следует добавить к проекту модуль UC112, который был создан в предыдущем разделе, и в модуле реализации активной формы сослаться на этот модуль. Сохраним проект под именем АХС11 ent. Далее сделаем этот элемент управления ActiveX доступным в Интернете (или в интрасети) — как это дела- ется, было подробно рассказано в главе 2. Единственное отличие — на вкладке
536 Глава 12. Технология DataSnap Project в предлагаемом диалоговом окне Web Deployment Options (рис. 12.10) следует установить флажок Deploy additional files и на вкладке Additional Files до- бавить файл Midas.dll (в версиях Delphi 3 и 4 он назывался dbclient.dll), который находится в каталоге Windows\system (или Winnt\System32). Рис. 12.9. Создание шаблона компонентов для тонкого клиента Рис. 12.10. Установка параметров поставки элемента управления ActiveX
Создание клиентских приложений в виде активных форм 537 Для переноса созданного OCX-файла на web-сервер следует выбрать команду Project ► Web Deploy. При этом будут созданы соответствующий набор файлов и HTML-страница с минимальным набором тегов. В дальнейшем полученную страницу можно отредактировать. После создания и переноса файлов и web-страницы на web-сервер можно от- крыть эту страницу в браузере, настроив уровень безопасности браузера так, чтобы он мог отображать элементы управления ActiveX (см. главу 2). Если уровень безопасности браузера позволяет отображать неподписанные элементы управления ActiveX, после загрузки web-страницы созданная актив- ная форма будет видна в браузере (рис. 12.11). Можно убедиться, что все ее ин- терфейсные элементы работают так же, как и интерфейсные элементы в обыч- ном приложении. Рис. 12.11. Отображение активной формы в браузере Отметим также, что можно обратиться к созданной HTML-странице с другого компьютера локальной сети, а также через Интернет. Нередко при создании таких элементов управления ActiveX задаются вопросы типа: «Можно ли создать элемент управления ActiveX, содержащий несколько
538 Глава 12. Технология DataSnap форм?». Ответ достаточно прост: в этом случае возможна генерация дополни- тельных форм (отображаемых уже не внутри браузера, а отдельно от него) дина- мически при наступлении какого-либо события (например, щелчка на кнопке). Нужно только поместить дополнительную форму при установке параметров проекта в список Available Forms и не забыть уничтожить созданную динамиче- ски форму, когда она станет не нужной. Следует также помнить, что библиотека OCX, в которую входит элемент управления ActiveX, содержащий несколько форм, будет иметь больший размер, чем в случае элемента управления ActiveX с одной формой. Если же необходимо поместить на web-страницу одновре- менно несколько форм, то на странице делается ссылка на несколько элементов ActiveX, при этом не обязательно, чтобы все они находились в одном файле с расширением *.осх. Проблемы отображения клиентских приложений в браузерах Нередко тонкий клиент, реализованный в виде элемента управления ActiveX, не отображается в браузере. Причин такого поведения может быть несколько, все они уже обсуждались в главе 2, и здесь мы лишь кратко их перечислим. Первая причина связана с тем, что далеко не все браузеры поддерживают воз- можность отображения элементов управления ActiveX с помощью тега <OBJECT>. Для отображения ActiveX следует использовать Microsoft Internet Explorer вер- сии 3.01 и выше (отметим, что в комплект поставки некоторых ранних 32-раз- рядных версий Windows входит более ранняя версия этого браузера); браузеры других производителей должны быть оснащены соответствующим модулем рас- ширения (plug-in). Вторая причина может быть, как было указано выше, связана с настройкой уровня безопасности браузера. Пользователь, желающий выполнять элемент ActiveX под управлением браузера, должен в общем случае дать разрешение на это — ведь ActiveX содержит исполняемый код, и нет никакой гарантии, что он безопа сен. Поэтому если элемент управления ActiveX не имеет электронной подписи, при использовании настроек браузера по умолчанию он выполняться не будет, а некоторые версии Internet Explorer при этом еще и не сообщают пользователю о том, что элемент ActiveX не был выполнен. Чтобы выполнить неподписанный элемент управления ActiveX, в параметрах безопасности браузера нужно явным образом указать, что пользователь разрешает выполнять код в элементах управ- ления ActiveX, полученных либо с конкретного web-сервера, либо с любого сер- вера в Интернете. Есть и третья возможная причина, о которой вспоминают в последнюю оче- редь, — операционная система может быть настроена так, чтобы пользователю было запрещено изменять реестр, и в этом случае элемент управления ActiveX в нем, естественно, не зарегистрируется. Если элемент управления ActiveX не отображается в браузере, несмотря на наличие разрешения на выполнение, следует убедиться, что в комплект поставки
Дополнительные возможности DataSnap-приложений 539 входят все необходимые для его выполнения файлы. Например, приложение может быть разбито на пакеты (packages), которые ошибочно не включены в комплект поставки. В этом случае рекомендуется проверить параметры проекта и, если не- обходимо, добавить недостающие файлы. Помимо этого, при поставке тонких клиентов следует не забыть включить в комплект поставки библиотеку Midas.dll (если приложение создается в Delphi 5 или выше) или dbclient.dll (если приложение создается в Delphi 3 или 4) и убе- диться, что серверные библиотеки DataSnap присутствуют на сервере доступа к данным. Дополнительные возможности DataSnap-приложений Ранее был создан простейший клиент, который использовал технологию DataSnap для отображения и редактирования содержимого одной таблицы. Однако в ре- альных проектах этого недостаточно — нередко требуется инициировать из кли- ентского приложения выполнение запросов, редактировать несколько связанных таблиц, использовать несколько модулей данных. Об этом пойдет речь в данном разделе. Создание связи «один ко многим» в технологии DataSnap Отличие DataSnap-приложений от традиционных приложений клиент-сервер за- ключается в том, что для отображения связей «один ко многим» между таблицами следует описывать эту связь как в сервере доступа к данным, так и в клиентском приложении. Если в свойстве Options компонента TDataSetProvider указано значе- ние poFetchDetai 1 sOnDemand, клиенту пересылаются только те подчиненные записи, которые ссылаются на данную главную запись. Следовательно, сервер доступа к данным должен «знать» о существовании этой связи. Однако клиент может отсоединиться от сервера доступа к данным и работать локально. Соответствен- но, для корректного отображения данных клиентское приложение также должно «знать» о существовании этой связи. Откроем созданный ранее проект сервера доступа к данным и в модуль дан- ных поместим компонент TADOTable. В свойстве Connection компонента TADOTablе2 сошлемся на компонент TADOConnectionl, а свойство TableName установим равным Orders. Поместим в модуль данных компонент TDataSource, и в свойстве DataSet сошлемся на компонент TADOTabl е2. В компоненте TADOTabl е2 установим значение свойства MasterSource равным DataSourcel, и с помощью свойства MasterFields опишем для таблиц связь главная-подчииепная (master-detail). Для того чтобы данные были доступны клиентскому приложению, поместим в модуль данных компонент TDataSetProvider и в его свойстве DataSet сошлемся на компонент ADOTable2. На этом модификации в сервере доступа к данным можно считать за- конченными. Скомпилируем проект и запустим один раз на выполнение.
540 Глава 12. Технология DataSnap Модифицируем клиентское приложение для отображения главной и подчи- ненной таблиц. Для этого поместим на форму в ранее созданном проекте клиент- ского приложения компоненты ТСН entDataSet, TDataSource и TDBGrid. В свойстве RemoteServer компонента C11entDataSet2 сошлемся на SocketConnectionl, свойство ProviderName установим равным DataSetProvider2. Свойство MasterSource установим равным DataSourcel и, используя свойство MasterFields, свяжем наборы данных по полю CustomerlD. Свойство DataSet компонента DataSource2 установим равным ClientDataSet2, а в свойстве DataSource компонента DBGrid2 сошлемся на DataSource2. После окон- чания всех модификаций свойство Connected компонента SocketConnectlonl вновь установим равным False. Следует немного изменить обработчик события OnClick щелчка на кнопке Connect, а именно добавить строку: C11entDataSet2.Active := True; Теперь можно приступать к тестированию проекта. После его запуска и щелчка на кнопке Connect можно увидеть содержимое обеих таблиц (рис. 12.12). Рис. 12.12. Отображение главной и подчиненной таблиц в DataSnap-клиенте Можно убедиться, что при перемещении по записям главной таблицы содер- жимое подчиненной таблицы изменяется. Обсудим подробнее значение poFetchDetailsOnDemand свойства Options компонента TDataSetProvider. Если выбрано это значение, сервер доступа к данным пересыла- ет в клиентское приложение только те записи подчиненной таблицы, которые связаны с данной записью главной таблицы, а если не выбрано, пересылается вся подчиненная таблица целиком. В случае выбора этого значения сетевой трафик и время ожидания пользователем отклика от сервера обычно меньше. С другой стороны, при работе в локальном режиме без соединения с сервером доступа к дан-
Дополнительные возможности DataSnap-приложений 541 ным пользователь не сможет просматривать содержимое записей подчиненной таблицы, кроме тех, которые ссылаются на ту запись главной таблицы, которая была текущей в момент отсоединения от сервера. В локальный файл при исполь- зовании модели briefcase также попадут только записи подчиненной таблицы, ссылающиеся на текущую запись главной таблицы. Поэтому решение об исполь- зовании (или не использовании) значения poFetchDetailedOnDemand следует при- нимать, исходя из конкретных требований, предъявляемых к проекту. Также следует упомянуть об изменениях в модели briefcase при работе с не- сколькими таблицами в клиентском приложении. Конечно, можно поместить на форму несколько кнопок и содержимое каждой таблицы сохранять в отдельном файле. Но, как правило, так не делают. Компонент Cl 1 entDataSet обладает мето- дами SaveToStream и LoadFromStream. Обычно открывают один файл и в нем при по- мощи этих методов сохраняют все данные, имеющиеся в клиентском приложе- нии. Для реализации такого подхода необходимо в код нашего примера внести следующие изменения: procedure TForml.ButtonSCllek(Sender: TObject): var FS: TFileStream: MS: TMemoryStream: N: Integer: begin FS := nil: MS := nil: if OpenDialogl.Execute then try FS := TFileStream.CreateCOpenDialogl.FileName, fmOpenRead); MS := TMemoryStream.Create: FS.Read(N. SizeOf(N)): MS.SetSize(N): FS.Read(MS.Memory*. N): Cl i entDataSet2.LoadF romStream(MS): FS.Read(N. SizeOf(N)): MS.SetSize(N); FS.Read(MS.Memory*. N); Cl 1entDataSetl.LoadFromStream(MS); finally FS.Free: MS.Free: end: end: procedure TForml.Button4Click(Sender: TObject); var FS: TFileStream: MS: TMemoryStream: N: Integer:
542 Глава 12. Технология DataSnap begin if (not ClientDataSetl.Active) or (not ClientDataSet2.Active) then Exit; FS := nil; MS := nil: if SaveDialogl.Execute then try FS := TFileStream.Create(SaveDialogl.FileName. fmCreate): MS := TMemoryStream.Create; Cli entDataSet2.SaveToStream(MS); N ;= MS.Size; FS.Write(N. SizeOf(N)): FS.Write(MS.Memory", N); Cli entDataSetl.SaveToStream(MS); N := MS.Size; FS.Write(N. SizeOf(N)): FS.Write(MS.Memory". N); finally FS.Free; MS.Free: end; end; Код получается довольно сложным из-за того, что методы SaveToStream и LoadFromStream компонента TCIientDataSet реализованы некорректно, а именно при сохранении данных в потоке данных не сохраняется их размер. Попробуем в методе Button4Click вместо использования промежуточного экземпляра класса TMemoryStream выполнить следующие команды: ClientDataSet2.SaveToStream(FS); ClientDataSetl.SaveToStream(FS); Таким образом, сохранение выполняется непосредственно в файловый поток. Теперь в методе Button3Cl i ck при вызове обратной последовательности представ- ленных ниже команд прямой загрузки из файла компонент Cl 1 entDataSet2 попы- тается считать содержимое всего файла: Cli entDataSet2.LoadF romStream(FS): Cli entDataSetl.LoadFromStream(FS): Это ему не удастся, и при возникновении такой ситуации автоматически про- исходит обращение к серверу доступа к данным, откуда берутся недостающие данные. Если сервер доступа к данным недоступен, то данные в компонент TC11 entDataSet не загружаются и возможность их локального редактирования от- сутствует. Данный пример может быть легко обобщен для работы с тремя и более набо- рами данных. И еще один совет: полезно запаковать данные каким-либо алго- ритмом сжатия перед их сохранением в компоненте TMemoryStream. Это позволит уменьшить размер файлов в несколько раз.
Дополнительные возможности DataSnap-приложений 543 Использование запросов в DataSnap-приложениях В реальных проектах целиком данные загружаются только из небольших таблиц- словарей; для отображения же большей части данных используется SQL-запрос с предложением WHERE. SQL-запрос должен генерироваться в клиентском при- ложении и затем передаваться на сервер. В DataSnap-приложениях для пере- дачи SQL-запроса на сервер используется свойство CommandText компонента TC11 entDataSet. Это свойство появилось в Delphi, начиная с версии 5. В более ран- них версиях Delphi необходимо было создавать дополнительный метод в интер- фейсе сервера — как это делается, будет рассмотрено далее. Продолжим работу с предыдущими проектами. В сервер доступа к дан- ным поместим компонент TADOQuery. В его свойстве Connection сошлемся на TADOConnectionl. Для транспортировки данных в клиентское приложение исполь- зуем компонент TDataSetProvider, свойство которого DataSet установим рав- ным ADOQueryl. Далее следует выбрать значение poAllowCommandText для свойства Options компонента TDataSetProvider. По умолчанию этот флаг в свойство Options не включается, а без него запрос не будет передаваться на сервер доступа к дан- ным. Далее следует скомпилировать проект и запустить его один раз на выпол- нение. Перейдем к изменениям в клиентском приложении. Поместим на форму компо- нент TEdit — в нем будем создавать SQL-запрос. Добавим на форму компоненты TClientDataSet, TDataSource и TDBGrid. Свойства, которые необходимо изменить в этих компонентах, были описано ранее. Свойство ProviderName установим рав- ным DataSetProviderl. Поместим на форму кнопку, присвоим ее свойству Caption значение Query и в обработчике события, связанного со щелчком на этой кнопке, выполним следующий код: procedure TForml.Button5Click(Sender: TObject): begin if not SocketConnectionl.Connected then Exit; Cli entDataSet3.Close; ClientDataSet3.CommandText := Editl.Text: Cl 1entDataSet3.Open: end: Перед изменением свойства CommandText необходимо сделать неактивным ком- понент TClientDataSet — иначе произойдет исключение. Далее этому свойству присваиваем значение, содержащееся в компоненте TEdit. По существу, это SQL- запрос. При открытии компонента ClientDataSet3 этот запрос будет передан на сервер, и, если он корректен, клиентскому приложению будут возвращены дан- ные, удовлетворяющие запросу (рис. 12.13). Свойство CommandText используется также для выполнения хранимой проце- дуры на сервере; при этом оно должно содержать имя этой процедуры.
544 Глава 12. Технология DataSnap Рис. 12.13. Результат выполнения запроса на сервере доступа к данным Использование нескольких модулей данных на сервере доступа к данным Часто бывает необходимо разместить компоненты доступа к данным не в одном, а в нескольких модулях данных — большинство реальных проектов именно та- ковы. Но в технологии DataSnap каждый удаленный модуль данных представ- ляет собой COM-объект со своей фабрикой классов, и его необходимо созда- вать. При этом возникает и ряд других проблем, например ссылки на компонент TDataSetProvider не видны в клиентском приложении. Тем не менее несколько удаленных модулей данных на одном сервере доступа к данным использовать можно. По-настоящему удобно это делать с помощью Delphi 7 в связи с появле- нием компонента TSharedConnection. В более ранних версиях Delphi для работы DataSnap с несколькими модулями данных приходилось реализовывать намного более сложный код. Откроем созданный ранее проект сервера доступа к данным, выберем команду File ► New ► Other и на странице Multitier окна репозитария объектов выберем значок Remote data module. В появившемся диалоговом окне введем имя СОМ-класса Child, все остальные параметры оставим без изменений. Созданный модуль с ко- дом сохраним под именем UServ3. Во вновь созданный модуль данных поместим компоненты TADOTable и TDataSetProvider. Установим свойство TableName компо- нента TADOTable равным Products. Это значение надо ввести вручную, поскольку
Дополнительные возможности DataSnap-приложений 545 при неопределенном свойстве Connection или Connectionstring список таблиц базы данных, естественно, не появляется. Свойство DataSet компонента TDataSetProvider установим равным ADOTablel. Для удобства изменим свойство Provider.Name, при- своив ему значение ProviderChiId. Обратимся к редактору кода реализации этого модуля данных (UServ3.pas). Экземпляр этого модуля не будет создаваться автоматически в ответ на требова- ние клиента. Поэтому к созданному ранее интерфейсу ITest необходимо доба- вить метод для создания экземпляра удаленного модуля данных. Для создания экземпляра СОМ-объекта прежде всего необходимо сохранить ссылку на экземп- ляр фабрики классов, причем эта ссылка должна быть доступна из других моду- лей. Поэтому в секции интерфейса UServ3.pas определим переменную: var Chi 1dFactory: TComponentFactory: Кроме того, немного изменим секцию инициализации UServ3.pas: initialization ChildFactory := TComponentFactory.Create(ComServer. TChild. Class_Child, ciMultiInstance. tmApartment): Обратимся к редактору библиотеки типов. Прежде всего, во вновь созданном интерфейсе IChild потребуется метод, имеющий возможность передать ссылку на компонент TADOConnection, экземпляр которого находится в главном модуле. Создадим новый метод интерфейса с именем SetConnection, как это описывалось ранее, перейдем на страницу Parameters и добавим один параметр типа 1 ong. По- сле щелчка на кнопке Refresh панели инструментов окна редактора можно реа- лизовать этот метод: procedure TChild.SetConnection(Connection: Integer): begin ADOTablel.Connection := TADOConnection(Connection); end: Здесь для передачи ссылки на компонент TADOConnection используется приве- дение типов. Далее необходимо создать еще один метод интерфейса ITest, который создаст эк- земпляр IChild и возвратит ссылку на него. Для этого в редакторе библиотеки типов создадим свойство с именем ChildDM, предназначенное только для чтения (рис. 12.14). Тип этого свойства — IChild, его следует выбрать в раскрывающемся списке. После щелчка на кнопке Refresh можно реализовать метод: function TTest.Get_ChiIdDM: IChild: begin Result := ChildFactory.CreateComObjectInil) as IChild: Result.SetConnectiondnteger(ADOConnectionl)); end: В приведенном выше фрагменте кода с помощью фабрики классов создается экземпляр класса TChild и сразу же передается ссылка на ADOConnection. Далее проект необходимо скомпилировать и обязательно один раз запустить.
546 Глава 12. Технология DataSnap Рис. 12.14. Новые методы интерфейсов сервера доступа к данным Перейдем к модификации клиентского приложения. Поместим на форму ком- поненты TDBGrid, TDataSource, TCIientDataSet и TSharedConnection (рис. 12.15). Рис. 12.15. Клиентское приложение на этапе разработки
Дополнительные возможности DataSnap-приложений 547 Свойство SharedConnectionl.Parentconnection установим равным значению Socket- Connectionl, выбрав его в раскрывающемся списке. Свойство SharedConnectionl. Chi 1 dName установим равным значению Chi 1 dDM, также выбрав его в раскрывающем- ся списке. Свойство ClientDataSet4.Connection установим равным SharedConnectionl. Свойство ClientDataSet4.ProviderName изменим на Chi 1 dProvider (обратите внима- ние, что это единственный доступный компонент TDataSetProvider в раскрываю- щемся списке этого свойства). Далее все выполняется как обычно — компонент TDataSource ссылается на ClientDataSet4, а компонент TDBGrid — на TDataSource. Изменим процедуру OnCl 1 ck кнопки Connect, а именно, добавим в нее следую- щую строку: ClientDataSet4.Active := True: После компиляции и запуска можно щелкнуть на кнопке Connect и увидеть, что клиент обращается к данным, источники которых расположены во втором модуле данных (рис. 12.16). Рис. 12.16. Клиентское приложение во время выполнения Созданный проект легко обобщить на случай, когда используются не два, а не- сколько модулей данных. Соответственно, в интерфейсе главного модуля созда- ется несколько предназначенных только для чтения свойств, которые создают экземпляры и передают ссылки на дополнительные модули данных. В клиент- ском приложении используется несколько компонентов TSharedConnection, каждый из которых ссылается на свой дополнительный модуль данных.
548 Глава 12. Технология DataSnap Обращение к компонентам VCL из кода удаленного модуля данных ВНИМАНИЕ ---------------------------------------------- Этот проект необходимо тестировать на платформе Windows 2000, Windows ХР или Windows NT 4.0 с установленным пакетом Service Pack 5 или выше. Полезной информацией для администратора сервера доступа к данным явля- ется число клиентов, которые в данный момент подсоединены к серверу. Инфор- мацию такого рода получить достаточно просто, если учесть, что при соединении с каждым из клиентов создается экземпляр класса TRemoteDataModule, а после раз- рыва соединения он разрушается. Достаточно переписать метод AfterConstruction и деструктор, чтобы создать счетчик подключений. Продолжим работу с сервером доступа к данным, созданным ранее. Объявим глобальную переменную СИentCount типа Integer, начальное значение которой установим равным нулю: var ClientCount: Integer = 0; Перепишем методы AfterConstruction и Destroy. Значение счетчика будем вы- водить в заголовке формы: procedure TTest.AfterConstruct1 on: begin inherited: IncCClientCount); Forml.Caption := IntToStr(ClientCount): end; destructor TTest.Destroy: begin Dec (CH entCount): Forml.Caption := IntToStr(ClientCount): inherited: end: На первый взгляд этого достаточно. Скомпилируем проект сервера, запустим клиентское приложение и щелкнем на кнопке Connect. Вот тут и происходят не- приятности: оба приложения (клиентское и серверное) зависают и никакие дан- ные в элементах управления не появляются. В чем причина? Она в том, что для кода компонента RemoteDataModule созда- ется отдельный поток выполнения и до окончания отработки конструктора TRemoteDataModule все потоки выполнения, включая главный, блокируются. Такая технология позволяет избежать синхронизации доступа к данным при обраще- нии к ним из конструктора этого класса. В приведенном выше коде происходит обращение к форме для того, чтобы изменить ее заголовок. Начиная с Windows
Дополнительные возможности DataSnap-приложений 549 NT 4.0 SP5, на уровне операционной системы реализована следующая технология доступа к стандартным элементам управления: все изменения в них могут выпол- няться только из кода главного потока. Поэтому при выполнении следующей строки кода дочерний поток будет ждать, пока появится доступ к главному потоку: Forml.Caption : = IntToStr(Cl1entCount); В свою очередь, главный поток выполнения будет ждать завершения дочер- него — происходит описанная в главе 6 ситуация взаимной блокировки (deadlock). Для того чтобы избежать взаимной блокировки потоков, необходимо отсро- чить вывод данных в заголовок формы. Такая отсрочка называется асинхронной развязкой. Ее смысл заключается в том, что выполнение определенного фрагмента кода на некоторое время откладывается. Реализовать асинхронную развязку мож- но с использованием компонента TTimer или метода PostMessage. Для создания асинхронной развязки обратимся к главной форме сервера доступа к данным и определим константу: const WM_CHANGECAPTION = WMJJSLR + 244: Объявим в секции private формы обработчик сообщений Windows: procedure WMChangeCaption(var Message:TMessage): message WM_CHANGECAPTION; Затем реализуем его: procedure TForml.WMChangeCaption(var Message: TMessage): begin Caption := IntToStr(Message.WParam): end: В реализованных выше процедурах TTest.AfterConstruction и TTest.Destroy вме- сто изменения заголовка формы выполним следующий фрагмент кода: PostMessageCForml.Handle. WM_CHANGECAPTION. CHentCount, 0); Команда PostMessage посылает окну сообщение Forml.Handle, и это сообщение ставится в очередь. После этого продолжается выполнение кода. Когда весь код будет выполнен, приложение начинает извлекать из очереди сообщения и выпол- нять связанный с ними код. Когда очередь дойдет до сообщения WM_CHANGECAPTION, будет изменен заголовок формы. Но когда и после выполнения каких именно фрагментов кода это произойдет, заранее сказать невозможно. Перенос бизнес-правил в клиентское приложение Описание бизнес-правил в клиентском приложении обычно позволяет контроли- ровать пользовательский ввод данных без обращения к серверу доступа к данным (в этом случае обычно говорят о том, что приложение является интерактивным). Технология DataSnap предоставляет для этого ряд возможностей.
550 Глава 12. Технология DataSnap Чтобы проиллюстрировать эти возможности, создадим простейший сервер дос- тупа к данным наподобие того, что был описан в начале главы, с одним модулем данных, содержащим компоненты TADOConnection, TADODataSet и TDataSetProvider. В качестве источника данных будем использовать уже знакомую нам таблицу Customers базы данных Northwind из комплекта поставки Microsoft SQL Server. В проекте сервера создадим для компонента ADODataSetl набор объектов TFi el ds. Добавим ограничение на значение одного из полей, например: procedure TRDM.RemoteDataModuleCreate(Sender: TObject): begin ADODataSetlCountry.CustomConstrai nt := 'Country IS NOT NULL': ADODataSetlCountry.Constrai ntErrorMessage := 'Please input country name': end; Создадим также клиентское приложение, отображающее данные из таблицы Customers в компоненте TClientDataSet и позволяющее выполнять методы Applyllpdates и Cancel Updates этого компонента. Запустив клиентское приложение, попробуем создать запись с пустым значением поля Country. При попытке ее сохранить мы получим именно то сообщение об ошибке, которое содержится в приведенном выше коде: Please input country name Помимо ограничений на значения полей мы можем передавать так называемые расширенные атрибуты полей исходного набора данных, такие как DisplayLabel, Al i gnment. Чтобы расширенные атрибуты были включены в передаваемый набор данных, следует выбрать значение poIncFieldProps в свойстве Options компонента TClientDataSet. Отметим, что, помимо ограничений и расширенных атрибутов полей, со- держащихся в сервере доступа к данным, существуют ограничения, хранящиеся в серверной СУБД. Компонент TDataSetProvider обладает свойством Constraints, отвечающим за то, передаются или нет серверные ограничения вместе с данными в клиентское приложение. Однако такие ограничения должны быть импорти- рованы в словарь данных, доступный серверу доступа к данным в процессе его работы. Словари же данных могут быть созданы только для BDE-источников, а для других технологий доступа к данным они не предусмотрены. Сортировка данных в компоненте TClientDataSet Еще один способ повысить интерактивность клиентских частей DataSnap-прило- жений заключается в предоставлении пользователям возможности осуществлять с полученными данными некоторые манипуляции, такие как сортировка или, ин- дексирование, без обращения к серверу доступа к данным. Для сортировки данных в компоненте TClientDataSet можно использовать свой- ство IndexFieldNames. Помимо этого компонент TClientDataSet обладает методами Add Index и Del ete Index. Эти методы позволяют произвести сортировку данных на этапе выполнения.
Дополнительные возможности DataSnap-приложений 551 Для иллюстрации этой возможности на форму клиентского приложения, содержащую данные из компонента ТС I i entDataSet, добавим компонент TLi stBox и создадим обработчик события AfterOpen компонента TCI 1 entDataSet: procedure TForm2.Cl 1entDataSetlAfterOpen( DataSet: TDataSet): var 1: Integer; fn: String; begin // уничтожаем список полей ComboBoxl.Items.Clear: for i:=0 to ClientDataSetl.FieldList.Count-1 do begin // если по этому полю можно сортировать данные if (Cl ientDataSetl.Fiel ds.Fields[i] is TStringField) or (ClIentDataSetl.Fields.Fields[i] is TNumericField) or (ClIentDataSetl.Fields.Fields[i] is TDateTimeField) then begin fn := ClIentDataSetl.Fields.Fields[i].FieldName: // то добавляем имя поля к списку ComboBoxl.Iterns.Add(fn): //и создаем новый индекс ClientDataSetl.AddIndex(fn + 'Index', fn. [ixCaselnsensitive], ”. ”. 0); ClientDataSetl.IndexName := fn + 'Index': end; end; // выбираем значение по умолчанию ComboBoxl.Itemindex:=0; end: В этом обработчике мы заполняем компонент TComboBox именами полей, по зна- чениям которых можно осуществлять сортировку (это объекты типа TNumericField, TDateTimeField, TStringField и их наследники, такие как TWideStringField, TIntegerField и т. д.), и добавляем соответствующие индексы к набору данных, содержащемуся в компоненте TCIientDataSet. Еще один обработчик события связан с выбором пользователем имени поля, по которому осуществляется сортировка: procedure TForm2.ComboBoxlChange(Sender: TObject): var fn: String; begin fn := ComboBoxl.Items.Strings[ComboBoxl.Itemindex]; // изменяем индекс ClIentDataSetl.IndexName := fn + 'Index' end:
552 Глава 12. Технология DataSnap И, наконец, нам следует установить порядок сортировки записей в наборе данных перед его открытием: procedure TForm2.Cl 1entDataSetlBeforeOpen( DataSet: TDataSet); begin // устанавливаем порядок сортировки по умолчанию CHentDataSetl.IndexName := 'DEFAULT_ORDER'; end; Теперь на этапе выполнения в компоненте TListBox будет отображаться спи- сок полей компонента TCI 1 entDataSet, и выбор поля в этом списке приведет к пе- ресортировке записей (рис. 12.17). Address City Region PostalCode Country Phone Рис. 12.17. Выбор порядка сортировки записей в компоненте TClientDataSet Обсудив некоторые возможности по манипулированию данными, предоставляе- мые технологией DataSnap как таковой, вспомним, что DataSnap-сервер является COM-сервером. Это означает, что функциональность такого сервера может быть расширена за счет добавления дополнительных методов к его интерфейсу. При- мерам применения дополнительных методов для расширения функциональности серверов доступа к данным будет посвящен следующий раздел. Работа с библиотеками типов Удаленные модули данных могут обладать дополнительными свойствами и мето- дами, которые можно создать в редакторе библиотек типов. Все, что ранее было
Работа с библиотеками типов 553 сказано о серверах автоматизации (за исключением поддержки нотификаций), применимо и к DataSnap-проектам. Это означает, что программист может создавать свои методы с собственным списком параметров и они будут работать в этих проек- тах. Ниже рассмотрены некоторые возможности, которые реализуются в DataSnap- проектах. Аутентификация пользователей В ранее созданном проекте не использовались имя пользователя и пароль — они фиксировались в свойстве Connectionstring компонента TADOConnection. В реаль- ных же проектах фиксированные имя пользователя и пароль для доступа к серверу баз данных обычно не применяют. Имя пользователя и пароль несут в себе не только информацию о возможности доступа, но и обеспечивают раздельный дос- туп к таблицам базы данных. Например, таблицу со сведениями о зарплате имеет право редактировать бухгалтер, просматривать — руководитель предприятия, а для других пользователей она должна быть закрыта. Значит, чам понадобится реально использовать параметры аутентификации пользователей. Начнем изменения с сервера. Откроем библиотеку типов, отметим интерфейс ITest и создадим новый метод с названием Login. Перейдем на страницу Parameters и добавим два параметра: UserName и Password типа BSTR. Это имя пользователя и пароль, которые будут поступать с клиентского приложения. Полезно также сообщить клиентскому приложению, успешным или неуспешным был доступ к базе данных. Для этого добавим еще один параметр Result типа VARIANT BOOL * (указатель на переменную WordBool) и в столбце Modifiers вместо значения [in] укажем [out, retval], как показано на рис. 12.18. Рис. 12.18. Параметры метода Login
554 Глава 12. Технология DataSnap Щелкнем на кнопке Refresh панели инструментов редактора. После этого об- ратимся к свойству ADOConnectionl.Connectionstring и скопируем его значение в буфер обмена. В коде приложения определим строку-константу со значением, полученным из буфера обмена: const Connectionstring = 'Provider=SQLOLEDB. 1:Password's:' + 'Persist Security Info=True;llser ID=£s;' + 'Initial Catalog=Northwind;Data Source=TREPA;' + 'Use Procedure for Prepare=l;Auto Translate=True:' + 'Packet Size=4096:Workstation ID=TREPA'; Вместо значений Password и UserID вставим символы £s — это позволит ис- пользовать эту строку в операторе Format. Напишем следующий код для вновь созданного метода: function TTest.Login(const UserName, Password: WideString): WordBool; var S: String; begin S := Password; if Length(S) = 0 then S := ...: try ADOConnecti onl.Connect!onStri ng := FormatCConnectionString. [S, UserName]): ADOConnectionl.Connected := True; Result := True: except Result := False; end; end; Полученные из клиентского приложения имя пользователя и пароль встав- ляются в строку, и эта строка используется для связи с базой данных. При правильно определенных параметрах будет успешно осуществлено соединение с базой данных, и возвращенный результат окажется равным True. При ошибке в параметрах аутентификации пользователя возникает исключение и возвра- щается False. Из кода понятно, что пользователь сможет обратиться к данным с теми правами, которые ему дают параметры аутентификации. Для более на- дежной защиты базы данных от несанкционированного доступа рекомендуется оставить свойство ADOConnectionl.Connectionstring пустым. После этого с базой данных будет невозможно соединиться, минуя метод Login. Выполним изменения в клиентском приложении. Для этого на форму помес- тим два компонента TEdit, в первый из них будем вводить имя пользователя, а во второй — пароль. Изменим обработчик события OnClick кнопки Connect. В окон- чательном виде он выглядит следующим образом:
Работа с библиотеками типов 555 procedure TForml.ButtonlClick(Sender: TObject); begin if SocketConnectionl.Connected then begin SocketConnectionl.Connected := False: Buttonl.Caption := 'Connect'; end else begin SocketConnectionl.Connected := True; i f SocketConnecti onl.AppServer.Logi n(Edit2.Text. Edit3.Text) then begi n ClientDataSetl.Active := True: ClientDataSet2.Active ;= True; ClientDataSet4.Active := True: Buttonl.Caption := 'Disconnect': end else begin SocketConnectionl.Connected := False: Messagedg('Error in login parameters'. mtWarning, [mbOK], 0); end; end: end; Любой компонент TXXXConnection обладает свойством AppServer, в котором хра- нится ссылка на интерфейс IDispatch сервера приложения. Используя это свой- ство, можно вызывать методы сервера приложений, как это было описано ранее для сервера автоматизации при позднем связывании (см. главу 3). При успешном обращении к методу Login происходит соединение с сервером, и пользователь ра- ботает в обычном режиме. При неверном наборе параметров аутентификации пользователь предупреждается об этом и может снова их ввести (рис. 12.19). Рис. 12.19. Неправильно введенные параметры аутентификации
556 Глава 12. Технология DataSnap Следует также отметить, что большинство компонентов TXXXConnection обла- дают событием OnLogln. При помощи обработчика этого события нельзя ввести параметры аутентификации — событие OnLogl п носит информационный характер и передает на клиентское приложение имя пользователя и пароль, которые были введены на сервере доступа к данным при значении свойства Log 1 nPrompt, равном True. Ранее уже объяснялось, почему диалоговое окно Login не следует отобра- жать на сервере доступа к данным. Передача текстовых сообщений от клиента к серверу доступа к данным В большинстве проектов клиентское приложение и сервер доступа к данным на- ходятся на разных компьютерах, которые могут быть удалены друг от друга. При работе пользователей у них могут возникать вопросы. Конечно, эти вопросы можно обсудить по телефону или отправить почтовое сообщение администратору. Но можно также воспользоваться протоколом автоматизации для передачи тек- стовых сообщений на сервер доступа к данным, где их увидит администратор. Как обычно, сначала вносим изменения в код сервера. В интерфейс ITest до- бавляем новый метод UserMessage с параметром Value типа BSTR. Реализация этого метода выглядит следующим образом: procedure TTest.UserMessage(const Value: WideString): var P: Pointer: N: Integer: S: String; begin S := Value; N := Length(S): GetMem(P. N + 1): FillMemory(P, N + 1. 0); if N > 0 then Move(S[l], P". N): PostMessage(Forml.Handle. WMJSERMESSAGE, Integer(P). 0): end; В памяти создается буфер и в него перемещается содержимое переменной Value. Значение Value обязательно следует предварительно преобразовать в строку — иначе будет отображен только первый символ. Далее с помощью метода PostMessage (асинхронная развязка) обращаемся к главной форме, при этом ссылку на буфер передаем в параметре wParam. Поместим на главную форму компонент TListBox и объявим константу: WMJJSERMESSAGE = WMJJSER+245; Создадим обработчик этого сообщения Windows. Его реализация имеет вид: procedure TForml.WMUserMessage(var Message: TMessage): var P: PChar: S: String;
Работа с библиотеками типов 557 begin Р := Pointer(Message.WParam): S := Р: FreeMem(P): Li stBoxl.Iterns.Add(S); end; Содержимое буфера копируется в строку, освобождается память, а строка за- носится в список. Теперь можно перейти к реализации клиента. Для этого поместим на его форму компоненты TEdit и TButton и создадим следующий обработчик события, связан- ного со щелчком на кнопке: procedure TForml.Button6Click(Sender: TObject): begin if SocketConnectionl.Connected then SocketConnecti onl.AppServer.UserMessage(Edl t4.Text); end; В приведенном фрагменте кода сначала производится проверка доступности сервера и при положительном результате проверки вызывается только что соз- данный метод (рис. 12.20). Рис. 12.20. Получение сервером сообщений от клиента
558 Глава 12. Технология DataSnap Из показанных примеров видно, что возможности сервера автоматизации по- зволяют легко создавать необходимые методы для конкретных проектов. Такая гибкость делает технологию DataSnap незаменимой для проектов, которые долж- ны содержать много нестандартных методов. Нотификации в технологии DataSnap Нотификационные сообщения, реализуемые путем вызова сервером методов кли- ента, с использованием стандартных процедур в технологии DataSnap выполнить невозможно. При выборе значка Remote Data Module на странице Multitier репози- тария объектов (открывается командой File ► New ► Others) появляется диалого- вое окно мастера создания удаленного модуля данных, в котором отсутствует флажок Generate Event Support Code. Поэтому способом, описанным в главе 3, соз- дание нотификаций невозможно. Авторам известны неудачные попытки реали- зовать обсуждавшийся в этой главе механизм нотификаций прямым написанием кода. И этого следовало ожидать: модель DCOM работоспособна в пределах одного домена, в то время как протокол TCP/IP, используемый в компоненте TSocketConnecti on, свободен от подобных ограничений. Компонент TSocketConnection обладает свойством SupporCall backs. Это свойство появилось впервые в Delphi 5. Имеющаяся документация о применении этого свойства крайне скудна — в ней сказано, что следует устанавливать его равным True, если компонент TSocketConnecti on в клиентском приложении может выпол- нить маршалинг обратных вызовов (callbacks) с сервера. Какая-либо документа- ция и примеры отсутствуют, поэтому неясно, каким образом с помощью компо- нента TSocketConnecti on можно выполнять маршалинг. Однако эту проблему можно обойти, если, помимо порта 211, который по умолчанию задействуется компонентом TSocketConnection, установить связь через протокол TCP/IP по другому порту. Используя новый порт, можно организовать коммуникацию между клиентом и сервером. В Delphi 6 и ниже очень удобно организовать такую коммуникацию с использованием компонентов TCI i entSocket и TServerSocket. В Delphi 7 эти компоненты на палитре отсутствуют, поэтому вместо них будем использовать компоненты TCPServer и TCPClient со страницы Internet. Идеология передачи сообщений с сервера на клиент будет заключаться в следующем. 1. После успешного установления контакта с сервером доступа к данным клиент инициализирует второе соединение через сокет с сервером, например, через порт 5555. При этом серверу передается IP-адрес клиента по протоколу OLE Automation. 2. При наступлении какого-либо события на сервере (например, если какой-либо пользователь выполняет метод Applyllpdates) сервер инициирует передачу данных всем клиентам. В качестве данных для вызова метода Applyllpdates можно, например, использовать имя TDataSetProvider провайдера, выполнив- шего изменения. Имя — это строка, она передается без затруднений. 3. Клиентское приложение анализирует полученную строку и в зависимости от ее значения вызывает какой-либо метод (или осуществляет цепочку вызовов).
Нотификации в технологии DataSnap 559 При решении конкретной задачи изменения данных на сервере методом Applyllpdates для визуального эффекта можно вызвать метод Refresh компо- нента TClientDataSet. В реальных проектах этого делать не рекомендуется — пользователю, скорее всего, не понравится мелькание данных на экране. Ему надо просто сообщить об изменении данных, например, поменяв цвет какого- либо элемента управления. Откроем созданный ранее проект MidServ.dpr и на форму (не в модуль дан- ных!) поместим компонент TCPClient. Свойство RemotePort установим равным 5555. Далее вызовем редактор библиотеки типов и добавим новый метод IPAddress. В качестве параметра он принимает переменную типа WideString. Ожидается, что клиент будет вызывать этот метод сразу же после установки соединения с серве- ром и передавать с его помощью свой IP-адрес. Сервер будет его сохранять в пе- ременной класса FAddress: TTest = class(TRemoteDataModule. ITest) private FAddress: String, public property Address: String read FAddress: implementation procedure TTest.IPAddress(const Adress: WideString): begin FAddress := Adress: end: Далее сервер должен использовать эту переменную для отправки сообщения клиенту. Поэтому дополнительно определим свойство Address в секции public — это сделает переменную доступной для чтения. Кроме того, нам потребуется спи- сок клиентов для рассылки им сообщений. Точно так же, как и в аналогичном примере в главе 3, определим глобальную переменную Cl ientLi st: TThreadLi st, из- меним секции initialization и finalization и перекроем методы AfterConstruction и Destroy (смысл данного кода раскрывается в главе 3): type TTest = class(TRemoteDataModule. ITest) public procedure AfterConstruction: override: destructor Destroy; override: var ClientList: TThreadList = nil; implementation procedure TTest.AfterConstructi on;
560 Глава 12. Технология DataSnap var L: TList; begin inherited; try L := ClientList.LockList: L.Add(Self); finally ClientList.UnlockList; end; end: destructor TTest.Destroy; var L: TList: N: Integer; begin try L := Clientlist.LockList; N := L.IndexOf(Self); if N >= 0 then L.Delete(N): finally Cli entLi st.UniockLi st; end; inherited; end: Кроме того, имеет смысл отображать адреса клиентов, соединенных с сервером приложений. Однако следует учесть, что работа с протоколом TCP/IP в Windows реализована через механизмы отправки и обработки сообщений. Следовательно, при работе с протоколом TCP/IP периодически проверяется наличие сообще- ний в очереди. Это означает, что не должно быть кода, исполняемого длительное время, во время выполнения которого блокируется доступ к очереди сообщений. Более того, реализация протокола TCP/IP такова, что для получения или пере- дачи данных в очередь ставится несколько сообщений. То есть нельзя немедленно вызывать методы в ответ на получение данных — вызов таких методов должен быть отсрочен. Проще всего этого достичь посредством метода PostMessage, ко- торый упоминался выше в связи с асинхронной развязкой. Фактически все программирование с использованием сокетов требует асинхронной развязки. Продолжим работу над проектом. Воспользуемся кодом, который был создан выше для отображения сообщений пользователя. Изменим реализацию метода IPAddress: procedure TTest.IPAddress(const Adress: WideString); var N: Integer; P: Pointer:
Нотификации в технологии DataSnap 561 begin FAddress := Adress: N := Length(FAddress): GetMemlP. N + 1); FillMemorylP. N + 1, 0): Move(FAddress[l], P\ N): PostMessagelForml.Handle. WMJJSERMESSAGE, Integer(P), 0): end: Теперь при каждом соединении клиента его IP-адрес будет отображен на форме сервера. Далее, при успешном выполнении метода Applyllpdates вызывается обработчик события Applyllpdates компонента TDataSetProvider. В ответ на это не- обходимо разослать всем клиентам имя провайдера, причем метод для рассылки вызвать асинхронно в обработчике события AfterApplyUpdates. Для этого определим константу: WM_NOTIFYCHANGE = WM_USER + 6: Кроме того, напишем обработчик события Windows: procedure TForml.WMNotifyChangelvar Message: TMessage): var P: PChar: T: TTest; S: String; I: Integer; L: TList; begin P := Pointer(Message.WParam); S := P; try L := ClientList.LockList; for I := 0 to L.Count - 1 do if Integer(L[I]) <> Message.LParam then begin T := TTest(L[Ij): if Length(T.Address) > 0 then begin TcpClientl.RemoteHost := T.Address; try if TcpClientl.Connect then TcpClientl.Sendln(S); finally TcpCli entl.Di sconnect: end; end: end: fi nal1 у Cl 1entList.UniockLi st: end: FreeMem(P); end;
562 Глава 12. Технология DataSnap В параметре Message.wParam будет находиться указатель на буфер с именем провайдера, который выполнил метод Applyllpdates. Он преобразуется в строку, а сервер, используя список Connect! ons всех соединенных с ним клиентов, рассы- лает им значение этой строки. В Message. 1 Param находится ссылка на клиента, ко- торый вызвал метод Applyllpdates. Ему не следует посылать сообщения. Перейдем к модулю данных. Выделим все компоненты TDataSetProvider и соз- дадим для них общий обработчик событий AfterAppl yllpdates: procedure TTest. DataSetProvi der3AfterApplyllpdates ( Sender: TObject; var OwnerData: OleVariant): var S: String: N: Integer: P: Pointer: begin S := (Sender as TDataSetProvider).Name: N := Length(S): GetMem(P. N + 1); FillMemory(P. N + 1. 0); Move(S[l], P\ N): PostMessageCForml.Handle, WM_NOTIFYCHANGE. Integer(P), Integer(Self)): end: Имя провайдера копируется в строку, создается буфер, где хранится значение этой строки. После этого командой PostMessage асинхронно вызывается реализо- ванный ранее метод WMNotifyChange. На этом работу с сервером можно считать законченной. Откроем клиентское приложение и поместим на него компонент TCPServer. Установим его свойство Local Port равным 5555 — это значение должно совпадать с номером порта, вы- бранным ранее в серверном приложении. Теперь необходимо после успешного обращения к серверу доступа к данным передать IP-адрес клиента. Для этого внесем изменения в обработчик события OnCl i ck кнопки Connect, который теперь будет выглядеть так: procedure TForml.ButtonlClick(Sender: TObject): begin if SocketConnectionl.Connected then begin SocketConnectionl.Connected := False; TCPServerl.Close: Buttonl.Caption := 'Connect'; end else begin SocketConnectionl.Connected := True: i f SocketConnecti onl.AppServer.Logi n(Edi t2.Text. Edit3.Text) then begin TCPServerl.Open: S := TCPServerl.LookupHostAddr(TCPServerl.LookupHostName(’')): SocketConnectionl.AppServer.IPAddress(S);
Нотификации в технологии DataSnap 563 ClIentDataSetl.Active := True: ClientDataSet2.Active := True: ClientDataSet4.Active := True: Buttonl.Caption := 'Disconnect': end else begin SocketConnectionl.Connected := False: Messagedg('Error in login parameters'. mtWarning, [mbOK], 0): end: end: end; При получении данных компонент TCPServer вызывает метод OnAccept. В нем можно узнать содержание строки и выполнить какие-либо действия. Однако это тоже лучше делать асинхронно. Поэтому в клиентском приложении определим константу: const WM_NOTIFYCHANGE = WMJJSER +246: Затем создадим обработчик Windows-сообщений: procedure TForml.WMNotifyChangeCvar Message: TMessage); var P: PChar: S: String: I: Integer: begin P := Pointer(Message.WParam): S := P; for I := 0 to Componentcount - 1 do if Components[I] is TCIientDataSet then if ANSICompareText((Components[I] as TCIientDataSet).ProviderName, S)=0 then (Components[I] as TCIientDataSet).Refresh: end: В этом обработчике из буфера извлекается полученная с сервера строка. Из ранее реализованного кода мы знаем, что там находится имя компонента TDataSetProvider. Далее осуществляется анализ всех компонентов, находящихся на форме, и если компонент является экземпляром TCIientDataSet, то проверяется совпадение его свойства ProviderName со строкой, полученной с сервера. При совпадении этих значений вызывается метод TC11 entDataSet. Refresh. Теперь можно создать обработчик события TCPServer.Accept: procedure TForml.TcpServerlAccept(Sender: TObject: Cli entSocket: TCustomIpCli ent): var s: String; N: Integer;
564 Глава 12. Технология DataSnap Р: Pointer: begin s := ClientSocket.Receivein: N := Length(S): GetMemCP. N + 1): FillMemory(P. N + 1. 0): Move(S[l], P\ N): PostMessage(Handle, WM_NOTIFYCHANGE, Integer(P). 0): end; Используя свойство Recei vel n, можно прочитать данные, полученные клиен- том. Они копируются в буфер, а затем асинхронно вызывается созданный ранее метод WMNotifyChange. Запустим клиентское приложение в двух экземплярах, установим связь с сер- вером и изменим какое-либо значение в таблице Customers. После щелчка на кнопке Update можно увидеть, что изменения автоматически произошли и в дру- гом экземпляре клиентского приложения (рис. 12.21). Рис. 12.21. Изменение данных в клиентском приложении при вызове метода ApplyUpdates в другом клиентском приложении
Использование технологии DataSnap в однозвенных системах 565 Хотелось бы еще раз напомнить, что в реальных проектах нельзя автоматиче- ски менять данные — об их изменениях на сервере следует просто информиро- вать пользователя каким-либо иным способом. Аналогично можно реализовать и другие нотификационные сообщения. На- пример, сервер может передать строку «D'sconnect», информируя клиента о не- обходимости отсоединения. Программист должен самостоятельно решать, какие нотификации он хочет использовать и какие строки нужно передавать для их идентификации. Использование технологии DataSnap в однозвенных системах Нередко разработчики небольших приложений, использующих одну или несколько таблиц, испытывают некоторые удобства при их поставке. Эти неудобства связаны с необходимостью установки на компьютер пользователя библиотек, реализующих универсальные механизмы доступа к данным, а также клиентские части серверных СУБД. В процессе установки библиотек доступа к данным могут возникать разнообраз- ные проблемы, уже обсуждавшиеся в начале этой главы, такие как несовпадение версий библиотек, некорректно работающие установочные приложения и т. д. Отметим также, что корректное написание дистрибутива приложения далеко не всегда является гарантией его корректной работы в дальнейшем. Например, не все пользователи грамотно удаляют приложения с компьютера. Часто бывает, что ставший ненужным каталог просто стирается, при этом записи в реестре мо- гут сохраниться. Создание упрощенного приложения для работы с базами данных В сложных многопользовательских системах с серверными базами данных и с боль- шим количеством таблиц риск, связанный с описанными выше проблемами, обычно невелик, так как в силу высокой стоимости таких систем на предприятиях обычно устанавливаются жесткие корпоративные правила для пользователей, в том числе запрещающие несанкционированную установку программного обес- печения без ведома системного администратора. Но если приложение невелико и содержит одну-две таблицы, вряд ли для его использования можно требовать жесткого соблюдения корпоративных правил. Отметим также, что в этом слу- чае сами библиотеки доступа к данным могут существенно превышать по объему и само приложение, и поставляемые с ним данные. По этим причинам разработчики таких приложений нередко изобретают соб- ственные форматы данных, позволяющие просто считывать файл с диска. Как ни парадоксально, разработчики, занимающиеся созданием подобных приложений, даже имея клиент-серверные версии Delphi, просто не обращают внимания на страницу DataSnap палитры компонентов, считая, что компоненты этой страницы не предназначены для решения их задач. А ведь именно там и со- держится компонент TCI 1 entDataSet, позволяющий создать файл, в который можно
566 Глава 12. Технология DataSnap поместить несколько относительно небольших таблиц, и использовать его, не прибегая к громоздким механизмам доступа к данным. Компонент ТСН entDataSet, получив данные с сервера доступа к данным, по- зволяет сохранить содержимое своего кэша в файле и загрузить его оттуда. По- сле этого можно просто забыть о сервере доступа к данным и работать только с загруженным файлом. В этом случае никакие библиотеки доступа к данным, кроме midas.dll, просто не нужны. Создадим пример такого упрощенного приложения. Но прежде реализуем приложение для переноса данных из таблиц в файл, содержащий кэш компонента TCIientDataSet. Для этого создадим новый проект и поместим на его главную форму три компонента TTable, три компонента TDataSourse, один компонент ТСН entDataSet, один компонент TDBGrid и один компонент TDBNavi gator (последние два компонента нужны только для контроля и просмотра данных и, по существу, совершенно не обязательны). Установим свойства этих компонентов, представленные в табл. 12.1. Таблица 12.1. Свойства некоторых компонентов упрощенного приложения Компонент Свойство Значение DBGridl DataSource DataSource3 DBNavigator1 DataSource DataSource3 Tablet DatabaseName DBDEMOS TableName customer.db Active True DataSourcel DataSet Tablet Table2 DatabaseName DBDEMOS TableName orders.db IndexFieldNames CustNo MasterFields CustNo MasterSource DataSourcel Active True DataSource2 DataSet Table2 ТаЫеЗ DatabaseName DBDEMOS TableName iterns.db IndexFieldNames OrderNo MasterFields OrderNo MasterSource DataSource2 Active True ClIentDataSetl ProviderName Providerl Active True DataSource3 DataSet ClientDataSetl
Использование технологии DataSnap в однозвенных системах 567 Далее выберем в контекстном меню компонента Cl 1 entDataSetl команду Assign Local Data, в списке открывшегося окна выделим пункт Tablet (рис. 12.22) и щелк- нем на кнопке ОК. Рис. 12.22. Выбор источника данных для компонента TClientDataSet После этого в кэш компонента TClientDataSet будут загружены данные (рис. 12.23). - *1 - -ai DД ^орре Forml Рис. 12.23. Проект на этапе разработки 1354 Cayman Divers World Unlimited 1356 Tom Sawyer Diving Centre 1380 Blue Jack Aqua Center 1384 VIP Divers Club 1510 Ocean Paiadise |Addr1 4-976 Sugarloaf Hwy PO BoxZ-547 1 Neptune Lane PO Box 541 : 632-1 Third FrydenhO; =23-738 Paddington Lane ; 32 Main St PO Box 8745 Теперь в контекстном меню того же компонента выберем команду Save То File и в открывшемся диалоговом окне открытия файла введем имя файла, в котором будут храниться данные из кэша. Итак, файл с данными готов. Теперь можно создать наше приложение. Для этого достаточно просто уда- лить с формы компоненты Tablel, Table2, ТаЫеЗ, DataSourcel, DataSource2 — они больше не нужны. Есть две возможности создания таких упрощенных приложений. Самый про- стой из них — хранить данные непосредственно в исполняемом файле приложе- ния (если их объем невелик). Для этой цели следует в контекстном меню компо- нента TClientDataSet выбрать команду Load From File и указать имя сохраненного прежде файла. После этого данные из файла окажутся в ресурсах формы, в чем можно убедиться, открыв форму в текстовом представлении. Если скомпилиро-
568 Глава 12. Технология DataSnap вать такое приложение, его можно передать пользователю. Единственное, что требуется добавить в комплект поставки — файл midas.dll из каталога Winnt\ System32 (или Windows\System). Еще один вариант — выполнить метод LoadFromFi 1е компонента TCI i entDataSet в обработчике события OnCreate формы. В этом случае файл с кэшированными данными следует также включить в комплект поставки приложения, и объем его может быть достаточно велик (насколько именно — зависит от ресурсов рабочей станции, на которой используется такое приложение). Отметим, что файл может быть сохранен как в двоичном формате (*.cds), так и в формате XML (*.xml), и, начиная с версии Delphi 6, такой файл иногда называют таблицей MyBase. Отметим, что Delphi позволяет хранить в таком файле данные из нескольких связанных таблиц (именно это и было нами сделано). Поэтому в полученном на- боре данных имеется поле типа TDataSetField, предоставляющее доступ к подчи- ненным записям. В нашем случае это записи таблиц orders.db и items.db из базы данных DBDEMOS (рис. 12.24). JiForml TaxRgte- |Conta<t ' 6.5 Erica Norman 0 George Weathers 0 Phyllis Spooner 0 Joe 8atley 0 Chris Thomas 0 Ernest Barrett 0 Russell Christopher 0 Paul Gardner jidfcZ I IttiU “j laxPaie Рис. 12.24. Работа приложения, отображающего данные нескольких связанных таблиц jlastinvoKeOate 2/2/1995 1 05 03 AM 11/17/1994 210.33 10/18/1994 7 20 ЗОН I 1/30/1992 2 00 56 AM I 3/20/1992 9.35:40 AM [ 11/8/193411 22 08 РЦ 2/1/1995 6.45:23 PM I 11/9/19941-2222 AMI -3 , .--гс-г ... [freight...| Атёоз1РэЙ] T аЫеЗ 4 5 $0 00 $000 (DATASET) 0 $0 00 $4,674.00 (DATASET) 0 $G00 $17.781 00 (DATASET) | Отметим, что помимо настольных приложений сохраненные в файле кэширо- ванные данные могут быть использованы при создании демонстрационных вер- сий и прототипов клиент-серверных приложений, где затруднена или исключена в силу лицензионных ограничений возможность поставки полноценной версии СУБД. Приемы экономии места на форме В приложениях с базами данных, использующих несколько связанных таблиц, места на форме для их отображения, как правило, не хватает — это общая проблема проектирования интерфейсов подобных приложений. Компонент TCI 1 entDataSet может помочь и в этом случае.
Использование технологии DataSnap в однозвенных системах 569 Возьмем наше предыдущее приложение, содержащее три компонента ТТаЫе, три компонента TDataSource и компонент ТСН entDataSet, и добавим в него компонент TDataSetProvider. Установим значение его свойства DataSet равным Tablet. Затем свойство ProviderName компонента ClientDataSetl установим равным DataSetProviderl. Теперь наше приложение позволяет редактировать данные из всех трех таблиц, при этом интерфейс приложения окажется примерно тем же, что и па рис. 12.24. Единственное, о чем дополнительно следует позаботиться, это о пересылке отредак- тированных данных обратно в исходные таблицы с помощью метода Applyllpdates компонента TCIientDataSet. Обычно для этой цели к форме добавляют какой-либо интерфейсный элемент, инициирующий выполнение этого метода. Иногда этот метод добавляют к обработчику события AfterPost компонента TCIientDataSet: procedure TForml.Cl 1 entDataSetlAEterPost( DataSet: TDataSet); begi n Cli entDataSetl.ApplyUpdates(-1): end: Отметим, однако, что это не самый эффективный способ сохранения отредак- тированных записей, так как метод Post в данном случае выполняется локально, а метод Applyllpdates требует обращения к базе данных, и при использовании се- тевой СУБД лучше выполнять его не так часто, как метод Post. Сохранение содержимого таблиц в локальных файлах В заключение создадим приложение, позволяющее сохранять в локальных фай- лах компонента TCIientDataSet любые доступные таблицы. С этой целью создадим форму, содержащую два компонента TListBox, компо- нент TDBGrid, два компонента TEdit, два компонента TCommandButton и один ком- понент TCheckBox. Поместим также на форму компоненты TSaveDialog, TDatabase, TSession, ТТаЫе, TCIientDataSet, TDataSetProvider и TDataSource (рис. 12.25). Рис. 12.25. Проект на этапе разработки
570 Глава 12. Технология DataSnap Установим значения свойств этих компонентов так, как показано в табл. 12.2. Таблица 12.2. Свойства некоторых компонентов приложения для хранения содержимого таблиц Компонент Свойство Значение Sessionl SessionName MySession Databasel SessionName MySession LoginPrompt False DatabaseName MyDB Tablet SessionName MySession ClIentDataSetl ProviderName DataSetProviderl Active False DataSourcel DataSet Tablet DataSetProviderl DataSet Tablet DBGridl DataSource DataSourcel SaveDialogl Filter ClientDataSet Fi 1 es|*.cds|Al 1 files]*.* DefaultExt *.cds Создадим обработчики событий, связанные со щелчками на кнопках, выбо- ром пунктов в списках и созданием формы приложения: procedure TForml.FormCreateCSender: TObject): begin Sess 1 onl. GetAl i asNames (Li stboxl. I terns) ; end; procedure TForml.ButtonlClickCSender: TObject): begin ListBox2.Items.Clear; Tablet.Close: Databasel.Connected := False: Databasei.Params.Cl ear; Databasel.AliasName := ListBoxl.Items[ListBoxl.Itemindex]; Databasel.Params.Add('USER NAME='+Editl.Text): Databasel.Params.Add(’PASSWORD”’+Edit2.Text): Databasel.Connected := True: Sessi onl. GetTabl eNames ('MyDB'. ". not CheckBoxl.Checked. False. ListBox2.Items): end: procedure TForml.ListBox2Click(Sender: TObject): begin if ListBox2.Itemindex < 0 then Exit; Tablet.Close:
Использование технологии DataSnap в однозвенных системах 571 Tablel.TableName := ListBox2.ItemsEListBox2.Itemlndex]; Tablel.Open: end: procedure TForml.Button2C11ck(Sender: TObject): begin if SaveDialogl.Execute then begin ClientDataSetl.Active := True: OilentDataSetl.SaveToFile(SaveDialogl.FileName): ClientDataSetl.Active := False: end: end; В момент создания формы создается список всех доступных баз данных с по- мощью метода GetAl i asNames компонента TSession. При выборе пункта в этом спи- ске, вводе имени пользователя и пароля происходит соединение с соответствую- щей базой данных и создание списка ее таблиц. Флажок SQL server нужен для того, чтобы указать, нужно ли выводить в списке расширения для имен таблиц. При выборе имени таблицы в списке ее данные отображаются в верхнем из компонентов TDBGrid и заполняют кэш компонента TClientDataSet, отображаемый в нижнем из компонентов TDBGrid. При щелчке на кнопке Save to CDS file появля- ется диалоговое окно сохранения файла, в котором следует ввести имя файла для сохранения таблицы (рис. 12.26). • 3.XI DBDEMOS Name DefaultDD IBLOCAL BCDEMOS Т urpion Diversity Password STANDARD1 } NorthWind | MS Access Databc; dBASE Files Excel Files <4 Visual FoxPro Date Visual FoxPro T abl . dBase Files Word*. 5<Login -a Г SQLServer Save to CDS file BIOL'FE ' В COUNTRY.DB DELIVERY DB NEXTORD.DB ORDERS 9B ANIMALS.DBF CUSTOMER.DB EVENTS.DB r:. RESERVAT.DB VENUES.DB HOLDINGS.DBF INDUSTRY.DBF MASTER.DBF . CUSli.LY.DB Рис. 12.26. Работа приложения для хранения содержимого таблиц CustNd jCompany ; 1221 Kauai Dive Shoppe 1231; Unisco 1351 S,pht Diver 1354 Cayman D ivers World U nlimitcd 'jAddri 4-97E Sugarloaf TP0 BoxZ-547 И Neptune Lane PO Box 541 Этот проект может быть расширен за счет методов, позволяющих просматри- вать главные и подчиненные таблицы, выполнять сортировку по значениям по- лей и т. д.
572 Глава 12. Технология DataSnap Исторический экскурс По мере выхода новых версий Delphi, технология DataSnap (MIDAS) претерпе- вала заметные изменения. Наиболее радикальные изменения произошли при пе- реходе от версии 4 к версии 5. Например, вместо интерфейса I Provider использу- ется интерфейс lAppServer, вместо библиотеки dbclient.dll — midas.dll; компонент TProvider также больше не используется. Нередко MIDAS-проекты, созданные в Delphi 3 и 4, требуют существенной модификации при переносе их в более поздние версии Delphi. При переходе от Delphi 5 к Delphi 6 изменения были не столь существенные, и проекты, созданные в Delphi 5, компилируются в Delphi 6 либо без изменений, либо с незначительными изменениями. И наконец, при переходе от Delphi 6 к Delphi 7 вообще не требуется вносить никаких изменений в DataSnap-проекты, за исключением тех случаев, когда доступ к серверу доступа к данным осуществлялся с помощью компонента TCorbaConnection. Поскольку технология DataSnap существует давно, полезно рассмотреть все происшедшие в ней изменения, с тем чтобы уметь переносить в Delphi 7 MIDAS- проекты, выполненные в старых версиях Delphi. Если такой проект был сделан с помощью Delphi 3 или 4, то при его переносе необходимо учитывать измене- ния, которые произошли как при переходе от Delphi 4 к Delphi 5, так и при пере- ходе от Delphi 5 к Delphi 6. Переход от Delphi 4 к Delphi 5 Поддержка объектов, не хранящих информацию о состоянии Первое, на что следует обратить внимание в MIDAS 3 (версия MIDAS, появив- шаяся одновременно с Delphi 5), — это отсутствие интерфейса IProvider, который прежде экспортировался из удаленного модуля данных и с помощью которого клиент мог обращаться к содержащимся в удаленном модуле данных компонен- там TDBDataSet. Наличие такого интерфейса при всей его привлекательности имело один серьезный недостаток — его применение приводило к тому, что модуль данных оказывался COM-объектом, хранящим информацию о состоянии (stateful object). Это означало, что такой объект, будучи созданным в памяти сервера, дол- жен содержать данные, связанные с конкретным обслуживаемым им клиентом (например, содержимое открытых таблиц, положение указателя текущей записи, сведения о том, сколько записей из набора данных было отправлено клиенту и т. д.), даже если в данный момент клиент к серверу не обращается. А из этого следует, что такой сервер требует немало ресурсов, часть из которых расходуется впустую. В прежней версии DataSnap можно было описать удаленный модуль данных, указав, что он может создаваться в режиме Multiple Instance или Single Instance. В первом случае внутри одного процесса можно было создавать сколько угодно таких объектов, но обслуживались они по очереди, так как такое приложение имело один поток выполнения. Поэтому производительность сервера была невы- сока. Во втором случае каждый клиент инициировал запуск на сервере отдельного
Исторический экскурс 573 процесса, что требовало большего количества ресурсов. Кроме того, в удаленных модулях данных MIDAS 1 и MIDAS 2 можно было использовать только компо- ненты — потомки TBDEDataset, a BDE поддерживает не более 48 процессов. Альтернативой объектам, хранящим информацию о состоянии, являются так называемые объекты, не хранящие информацию о состоянии (stateless objects). После обмена данными с одним клиентом такой объект может быть задейство- ван другими клиентами, что позволяет «обобществлять» объекты, либо создавая соответствующий код, либо применяя средства, обеспечивающие такое коллек- тивное использование объектов, основанное на создании пулов объектов (object pooling), разделяемых многими клиентами; подобную возможность предостав- ляют, например, службы компонентов Microsoft (Microsoft Component Services). В результате таких объектов требуется обычно существенно меньше, чем в версиях MIDAS 1 и MIDAS 2. Иными словами, поддержка возможности «обобществле- ния» серверных объектов решает многие проблемы, связанные с производитель- ностью и ресурсами сервера. В MIDAS 3 альтернативой интерфейсу IProvider стал новый интерфейс lAppServer. Имея много общего с вышедшим из употребления интерфейсом IProvider, он тем не менее в силу особенностей своей реализации позволяет соз- давать модули данных, не хранящие информацию о состоянии — эта информа- ция теперь может храниться в клиентском приложении. Если нужно приспосо- бить такой модуль данных для коллективного использования, можно вызвать его метод ReglsterPooled. Справедливости ради надо заметить, что удаленные модули данных, не хра- нящие информацию о состоянии, можно было создавать и в предыдущих версиях MIDAS, однако вместо экспорта интерфейса IProvider создавались методы для передачи данных, изменения данных и т. д., что требовало написания больших объемов кода. Поддержка всех потомков класса TDBDataSet В MIDAS 3 из употребления выведен и компонент TProvider. Для совместимости с ранее созданными серверами он оставлен в библиотеке VCL, но отсутствует на палитре компонентов. Причиной отказа от этого компонента стало введение но- вого интерфейса IProvIderSuport, который предоставляет все методы, необходи- мые для доступа к потомкам TDBDataSet из клиентского приложения, и реализован в компоненте TDBDataSet и его потомках. Вместо компонента TProvider теперь используется компонент TDataSetProvider, свойство Exported которого можно устанавливать равным True. Благодаря этому с помощью интерфейса lAppServer можно получить доступ к набору данных, с ко- торым связан компонент TDataSetProvider. Компонент TDataSetProvider в новой версии DataSnap, в отличие от прежней версии, может предоставлять доступ к любым потомкам TDBDataSet (в силу под- держки ими интерфейса I Providersupport). Это, в свою очередь, позволяет ис- пользовать в удаленном модуле данных любые компоненты доступа к данным, являющиеся наследниками данного класса (например, компоненты со страниц ADO и Interbase), и тем самым исключить обязательность использования BDE при создании DataSnap-серверов.
574 Глава 12. Технология DataSnap Изменения в компоненте TDataSetProvider Как уже упоминалось, к компонентам доступа к данным на сервере обычно добавля- ется компонент TDataSetProvider. Некоторые свойства и события этого компонента совпадают с соответствующими свойствами и событиями компонента TProvider, но имеется и ряд новых. Начиная с версии MIDAS 3, компонент TDataSetProvider обладает, помимо характерной для данного компонента функциональности, всей функциональностью прежнего компонента TProvider. Это и стало причиной того, что последний оставлен в VCL только для совместимости и отсутствует в палитре компонентов. В MIDAS 3 список вложенных свойств свойства Options компонента TDataSetProvider был существенно расширен по сравнению с предыдущей версией. Рассмотрим некоторые наиболее интересные из них. Новое свойство Exported может принимать значение True (компонент TDataSetProvider виден удаленному клиенту) или False. В Delphi 4 действие, ана- логичное заданию значения для этого свойства, достигается выбором (или не выбором) команды Export ProviderXXX from data module в контекстном меню ком- понента доступа к данным (или компонента TProvider). Значения флагов poDisablelnserts, poDisableEdits и poDisableDeletes очевидны из их названий. Появление этих флагов определенно внесло гибкость в много- звенную архитектуру. Ранее (в Delphi 4) нескольким клиентам, которые обраща- лись к серверу доступа к данным, предоставлялись равные права на манипуля- ции данными. Если необходимо было разрешить редактирование данных хотя бы для одного клиента, то остальные клиенты автоматически также получали на это право. Теперь же сервер приложений может отдельным клиентам разрешить редактирование данных, другим запретить в рамках единственного соединения с сервером базы данных. ж poAllowMulti RecordUpdates — разрешает изменения в данных, которые затраги- вают несколько записей. При отсутствии этого флага такие изменения не до- пускаются. ж poNoReset — при обращении к методу lAppServer. AS_GetRecords из компонента TCI i entDataSet игнорирует флаг grReset. » poAutoRefresh — автоматически выполняет метод Refresh класса TCli entDataSet при каждом выполнении метода ApplyUpdates. ж poPropagateChanges — изменения, которые осуществляются с данными в обработ- чиках событий BeforellpdateRecord или AfterllpdateRecord класса TDataSetProvider, автоматически передаются клиенту, где они объединяются с имеющимися данными. ж poAllowCommandText — позволяет во время выполнения изменять SQL-запрос компонента TQuery (или аналогичных компонентов для других механизмов доступа к данным), расположенном на сервере приложений, или название хранимой процедуры компонента TStoredProc (или других компонентов дос- тупа к хранимым процедурам). Только при установке этого флага можно использовать метод Execute и свойство CommandText клиентского компонента TClientDataSet (см. ниже).
Исторический экскурс 575 Нередко DataSnap-серверы, помимо доступа к данным, обладают и другой функциональностью, позволяющей реализовать те или иные бизнес-правила или просто выполнить какие-то действия (в том числе изменить данные). Изменение данных может осуществляться также сервером баз данных (например, при авто- матической генерации первичных ключей, выполнении триггеров и хранимых процедур и т. д.). В прежних версиях DataSnap об изменении данных клиент мог узнать, только получив с сервера все содержимое соответствующего компо- нента TClientDataSet. В MIDAS 3 можно установить равным True значение свой- ства Options.poPropagateChanges компонента TDataSetProvider, после чего все изме- нения, происходящие на сервере, будут автоматически передаваться клиентам в их компоненты TClientDataSet. Кроме того, компонент TDataSetProvider может сохранять изменения непосред- ственно на сервере баз данных (а не только в наборе данных, хранящемся в его кэше). Свойство Options.poAllowCommandText просто позволяет получить от клиента SQL-запрос в виде текста и выполнить его — соответствующий метод уже реали- зован в этом компоненте (в прежних версиях такой метод приходилось создавать вручную). Помимо этого, у данного компонента имеются свойства Options.poDisableEdits, Options.poDisablelnserts и Options.poDisableDeletes, с помощью которых можно запретить вносить соответствующие изменения в компоненте TCl ientDataSet, что, в свою очередь, позволяет правила редактирования данных сосредоточить на сервере доступа к данным. Компонент TDataSetProvider имеет также ряд новых событий. Новые пары — AfterApplyUpdates и BeforeApplyUpdates, AfterExecute и BeforeExecute, AfterGetParams и BeforeGetParams, AfterGetRecords и BeforeGetRecords, AfterRowRequest и BeforeRowRequest имеют тип TRemoteEvent и взаимодействуют с одноименными методами, определен- ными в классе TClientDataSet. Они генерируются при вызове методов Applyllpdates, Execute, FetchParams класса TClientDataSet и при считывании данных с сервера доступа к данным (изменения свойства Active). Последовательность вызова этих методов следующая (на примере вызова метода Applyllpdates). 1. В клиентском приложении при выполнении кода встречается вызов метода: Applyllpdates 2. Клиентское приложение вызывает обработчик события: TCIi entDataSet.BeforeApplyUpdates 3. Клиентское приложение обращается к серверу посредством вызова метода: IAppServer.AS_ApplyUpdates 4. Серверное приложение вызывает событие: TDataSetProvi der.BeforeApplyUpdates 5. На сервере выполняются необходимые изменения в данных, при этом созда- ется новый пакет данных, в котором содержатся все изменения.
576 Глава 12. Технология DataSnap 6. Серверное приложение вызывает обработчик события: TDataSetProvider.AfterApplyUpdates 7. Клиентское приложение вызывает обработчик события: TC11entDataSet.AfterApplyUpdates 8. Продолжается выполнение кода клиентского приложения. Данная последовательность может быть нарушена при возбуждении исключе- ния или, например, при наличии ошибки Reconci 1 eError. Для других команд кли- ентского приложения последовательность событий выглядит аналогично. Помимо уведомления о возникновении той или иной ситуации данные обработчики со- бытий могут быть использованы для передачи приватных данных (private data), то есть данных, которые не предусмотрены в стандартной реализации технологии DataSnap, но которые, исходя из особенностей проекта, требуется передавать на сервер и/или клиент. Для понимания механизма передачи подобных данных следует обратить внимание на то, что все эти события имеют тип TRemoteEvent, который определен следующим образом: TRemoteEvent = procedure!Sender: TObject: var OwnerData: OleVariant) of object: В переменную OwnerData можно помещать любые данные, при этом имеет ме- сто описанная ниже последовательность действий. 1. В обработчике события BeforeXXXX компонента TCI IentDataSet данные помеща- ются в переменную OwnerData. 2. В обработчике события BeforeXXXX компонента TDataSetProvider можно считать значение переменной OwnerData. Заполнять эту переменную здесь не имеет смысла — ее значение не будет никуда передаваться. 3. В обработчике события AfterXXXX компонента TDataSetProvider следует помес- тить данные в переменную OwnerData. Считывать ее не имеет смысла — ее зна- чение не определено. 4. И, наконец, в обработчике события AfterXXXX компонента TCI IentDataSet можно получить данные, определенные в п. 3. Например, если администратору сервера доступа к данным необходима инфор- мация о клиентах, которые вызывали метод Applyllpdates, то на форму сервера доступа к данным можно поместить компонент TListBox и создать один общий обработчик события BeforeApplyUpdates для всех компонентов TDataSetProvider: procedure TTest.DataSetProviderlBeforeApplyUpdates( Sender: TObject; var OwnerData: OleVariant): var S; String; begin S := (Sender as TDataSetProvider).Name + ’ ' + OwnerData + ' ' + DateTimeToStr(Now): Forml.Li stBoxl.Items.Add(S): end;
Исторический экскурс 577 Соответственно, в клиентском приложении создаются обработчики события BeforeApplyllpdates для компонентов TClientDataSet: procedure TForml.Cl 1entDataSetlBeforeApplyUpdates( Sender: TObject; var OwnerData: OleVariant): begin OwnerData := 'MyName': end: При выполнении метода Applyllpdates компонента TClientDataSet, помимо из- менений данных на сервере базы данных, на сервере доступа к данным появляется визуальная информация (рис. 12.27). Рис. 12.27. Результат обработки события AfterApplyUpdates Среди этой визуальной информации имеется и строка «MyName», определен- ная в клиентском приложении. В Delphi 4 для аналогичной передачи данных требуется создать новый метод в интерфейсе-потомке IDataBroker стандартным для сервера автоматизации способом — с помощью редактора библиотеки типов. Соответственно, в клиентском приложении везде, где вызывается метод ApplyUpdates, необходимо вызвать и этот метод. В Delphi 5 можно сделать то же самое, поэтому приведенный выше пример можно использовать для расширения возможностей обмена данными. Обратный процесс передачи приватных данных — от сервера к клиенту — требует реализации обработчиков событий TDataSetProvider.AfterApplyUpdates на сервере и TClientDataSet.AfterApplyUpdates в клиентском приложении. Способ реализации остальных пар событий (Execute и т. д.) аналогичен. Компонент TDataSetProvider обладает также новым обработчиком события OnGetTabl eName. Этот обработчик вызывается в тех случаях, когда метод ApplyUpdates применяется к данным, полученным с использованием компонента TQuery со зна- чением False свойства RequestLive (или со значением True, но при нередактируе- мом результате запроса) или компонента TStoredProc. Программист, создающий сервер доступа к данным, должен использовать этот обработчик события для указания имени таблицы, в которой будут происходить изменения. Во многих методах компонента TDataSetProvider появился новый параметр — набор данных Delta, потомок класса TDataSet. Этот набор данных является не- пустым, если в данные вносятся изменения. При этом все изменявшиеся записи копируются в переменную Del ta, в которой сохраняются как оригинальные зна- чения полей отредактированных записей, так и измененные.
578 Глава 12. Технология DataSnap Хотелось бы обратить внимание, что, начиная с Delphi 5, компоненты TDataSetProvider и TClientDataSet не требуют, чтобы источником данных являлся потомок класса TDBEDataSet; иными словами, начиная с Delphi 5, можно создать сервер доступа к данным, который не использует BDE. Отметим также, что именно в этой версии Delphi компонент TProvider был заменен компонентом TDataSetProvider, который реализует все функции TProvider. Модуль, описываю- щий функциональность компонента TProvider, оставлен с целью переносимости унаследованного кода. Изменения в интерфейсах Начиная с версии Delphi 5, были изменены интерфейсы, доступные в библиоте- ках типов серверов доступа к данным. В Delphi 4 экспонировался интерфейс — потомок интерфейса IDataBroker, который, в свою очередь, обладал одним ме- тодом GetProviderNames, добавленным к методам интерфейса IDispatch. Начиная с версии Delphi 5, в библиотеках типов серверов доступа к данным экспониру- ется интерфейс lAppServer, к которому добавлен ряд методов — AS_ApplyUpdates, AS_GetRecords, AS_DataRequest, AS_GetProviderNames, AS_GetParams, AS_RowRequest, AS_Execute. Интерфейс IDataBroker тем не менее сохранился в Delphi 5 для компиля- ции приложений, код которых был создан в Delphi 4. Ранее аналоги этих методов (кроме AS_GetProvi derNames и AS_Execute) вызывались с помощью методов интерфейса IProvider. Исправлена ошибка, имевшаяся в реализации MIDAS в Delphi 4, — обращение к данным теперь защищено критическими секциями (в Delphi 4 при наличии нескольких клиентов, подключенных к одному серверу доступа к дан- ным, и пои больших объемах данных, получаемых в результате запросов к серверу баз данных, периодически возникали сбои, связанные с доступом к данным из разных потоков). Изменилась и идеология динамического экспонирования данных, в частно- сти вызова конструктора класса TDataSetProvider во время выполнения приложе- ний. В случаях когда заранее неизвестно число компонентов доступа к данным, нуждающихся в экспонировании, в Delphi 4 можно было динамически создать объект TProvider и описать ссылку на него в библиотеке типов как элемента коллек- ции IProvider. После этого такой объект становится доступным удаленному кли- енту. В Delphi 5 также можно динамически создать компонент TDataSetProvider, но описывать ссылку на него в библиотеке типов не требуется. Вместо этого необходимо вызвать метод Registerprovider класса TRemoteDataModule, и после этого компонент TDataSetProvider становится доступным для работы с удаленным кли- ентом. Изменения в компоненте TSocketConnection Компонент TSocketConnecti on, начиная с Delphi версии 5, приобрел новое свойство SupportsCall backs. Значение этого свойства, равное True, означает, что клиентское приложение принимает нотификации от сервера приложений. Нотификации в тех- нологии DataSnap чрезвычайно важны. Например, если с сервером доступа к дан- ным работают несколько клиентов и один из них изменил данные, то желательно
Исторический экскурс 579 проинформировать об этом других клиентов, чтобы они могли считать новый набор данных. К сожалению, примеров нотификаций с использованием данного свойства в комплектах поставки Delphi 5 и Delphi 6 нет, а его описание в доку- ментации довольно скудное. Если установить это свойство равным False, можно не заботиться об установке библиотеки WinSock 2, если клиент выполняется под управлением Windows 95. Еще одна возможность, появившаяся в Delphi 5, связана с расширением средств обеспечения безопасности. В принципе компонент TSocketConnection позволяет удаленно запускать любые серверы автоматизации, если на содержащем их компь- ютере запущено приложение Borland Socket Server. Теперь можно добавлять в ре- естр сведения о том, разрешен или нет удаленный запуск конкретного сервера с помощью конкретного протокола (это делается путем внесения изменений в ме- тод UpdateRegistry удаленного модуля данных, генерируемый при его разработке, с помощью функций EnableSocketTransport и Di sabl eSocketTransport), а при запуске приложения Borland Socket Server включить в его меню команду Registered Objects Only. Начиная с версии Delphi 5, у компонентов TXXXConnection отсутствует метод GetProvider. Вместо этого был добавлен внутренний метод GetServer, который возвращает ссылку на интерфейс lAppServer. Вместо прямого вызова этого метода следует обращаться к свойству AppServer так же, как и в Delphi 4. Поддержка протокола HTTP и компонент TWebConnection Компонент TWebConnecti on, предназначенный для доступа к серверам по протоколу HTTP и уже рассмотренный нами в главе 11, также впервые появился в Delphi 5. Начиная с этой версии, при соединении с сервером можно использовать бранд- мауэры и протокол SSL. При использовании этого компонента, как и при использовании компонента TSocketConnecti on, можно также указать в реестре Windows, доступен ли кон- кретный сервер автоматизации для удаленного доступа по протоколу HTTP. Делается это с помощью функций EnableWebTransport и DisableWebTransport. Изменения в компоненте TCIientDataSet Компонент TCIientDataSet, начиная с Delphi версии 5, также приобрел ряд новых свойств и событий. Во-первых, следует отметить появление ограничений, приме- нимых к записям. Во-вторых, как уже было сказано ранее, запретить выполнять те или иные операции редактирования в компоненте TCIientDataSet удобнее всего в серверном приложении, используя соответствующие свойства компонента TDataSetProvider. В-третьих, при выполнении хранимых процедур их выходные параметры, начиная с Delphi версии 5, передаются в клиентское приложение ав- томатически, поэтому принудительный вызов метода Fetch Ра rams, требовавшийся при создании таких приложений в Delphi 4, теперь не требуется. Метод SendParams компонента TCIientDataSet теперь также отсутствует. Отметим еще, что свойства HasProvider и Provider компонента TCIientDataSet переименованы в HasAppServer и AppServer соответственно.
580 Глава 12. Технология DataSnap Начиная с Delphi версии 5, можно заставить сервер доступа к данным выпол- нить SQL-запрос (в том числе запрос, не возвращающий набор данных). Для этой цели, помимо установки соответствующих свойств компонента TDataSetProvider, следует просто поместить текст запроса в свойство CommandText компонента ТСН entDataset и выполнить метод Execute. Параметры запроса можно передать, воспользовавшись свойством Params. Свойство CommandText позволяет естественным для программиста образом из- менить содержимое SQL-запроса или имя хранимой процедуры на сервере дос- тупа к данным и в результате получить новый набор данных. В Delphi 4 также можно было во время выполнения приложения заменить SQL-запрос новым, определенным в клиентском приложении. Для этого необходимо было исполь- зовать редактор библиотеки типов сервера доступа к данным и добавить новый метод (назовем его NewQuery) к интерфейсу — потомку IDataBroker. Этот метод в качестве параметра должен был содержать константу типа WideString. В его реализации эта константа присваивается свойству SQL.Text компонента TQuery, при необходимости изменяя свойство Active. Клиентское приложение могло вы- звать этот метод, используя свойство AppServer компонента, ответственного за связь с сервером приложений (TSocketConnection или TDCOMConnection): procedure TForml.ButtorlClick(Sender: TObject): begin Cli entDataSet1.Close: SocketConnectionl.AppServer.NewQuery(Memol.Text): ClientDataSetl.Open; end: В Delphi версий 5 и выше также можно воспользоваться указанным выше способом получения другого набора данных, но изменение запроса при помощи свойства CommandText более естественно. Чтобы изменение этого свойства приво- дило к изменению запроса, необходимо выполнение двух условий. Ж компонент TClientDataSet (размещаемый в клиентском приложении) через свое свойство ProviderName должен ссылаться на компонент TDataSetProvider (размещаемый на сервере доступа к данным), который, в свою очередь, через свое свойство DataSet должен ссылаться на какой-либо компонент доступа к данным, реализующий выполнение запроса к базе данных; Ж в свойстве Options компонента TDataSetProvider сервера должен быть установ- лен флаг poAllowCommandText. Получение нового набора данных для представленного выше примера осуще- ствляется следующим образом: procedure TForml.ButtonlClick(Sender: TObject): begi n Cl i entDataSetl.Close: ClientDataSetl.CommandText := Memol.Text: ClientDataSetl.Open; end:
Исторический экскурс 581 При этом в Delphi версии выше 5, в отличие от Delphi 4, не требуется созда- вать новый метод на сервере доступа к данным. Следует обратить внимание, что SQL-запрос обязательно должен возвращать набор данных. Если он не воз- вращает набор данных, то необходимо использовать метод Execute компонента TClientDataSet (см. ниже). Помимо этого, список событий компонента TClientDataSet пополнился но- выми событиями вида BeforeXXX и AfterXXX (BeforeApplyUpdates, AfterApplyUpdates, BeforeGetRecords, AfterGetRecords и др.). Эти события позволяют более гибко управлять данными, передаваемыми на сервер, а также служат для хранения сведе- ний о состоянии данных, связанных с конкретным клиентом, в самом клиентском приложении, позволяя тем самым не хранить их в удаленном модуле данных (полезность такого подхода уже обсуждалась выше). Эти события происходят перед исполнением и после исполнения методов компонента TClientDataSet: Applyllpdates, Execute, FetchParams, DoGetRecords, Refresh, DoRowRequest и могут исполь- зоваться для уведомлений о вызовах этих методов. При этом методы DoGetRecords и DoRowRequest недоступны для вызова из экземпляра класса (они определены в секции protected класса TClientDataSet) и являются частью системы обмена данными между сервером и клиентом. Все эти методы, кроме Execute, имеют ана- логи в Delphi 4, но в Delphi 4 для них отсутствовали приведенные выше собы- тия. Помимо организации нотификационных сообщений данные события можно использовать для передачи данных от сервера приложений к клиенту, и наобо- рот, как это было описано выше. Начиная с Delphi 5, компонент TClientDataSet обладает тремя новыми методами, которые экспонируются в секции public и доступны для вызова из экземпляра класса. Это SetProvider, DataRequest и Execute. Метод SetProvider используется для назначения компонента TDataSetProvider экземпляру TClientDataSet в тех случаях, когда клиент и сервер доступа к данным реализованы в одном и том же прило- жении. Такая ситуация возникает, например, когда разработчик сервера доступа к данным хочет отобразить последний набор данных, к которым был применен метод ApplyUpdates. Компонент TClientDataSet устанавливается в этом случае на сервере доступа к данным и стандартным способом связывается с компонентами отображения данных. Для всех экземпляров компонента TDataSetProvider в обра- ботчике событий AfterApplyUpdates используется следующий код: procedure TForml,DataSetProviderlAfterApplyUpdates( Sender: TObject: var OwnerData: OleVariant); begin Cl 1entDataSetl.Close; DBGri dl.Columns.Clear: ClientDataSetl.SetProvider(Sender as TDataSetProvider): ClientDataSetl.Open; end: Следует помнить, что метод SetProvider нельзя вызывать для удаленного ком- понента TDataSetProvider, который экспонируется через интерфейс lAppServer.
582 Глава 12. Технология DataSnap Метод DataRequest вызывает событие OnDataRequest удаленного компонента TDataSetProvider и возвращает новый набор данных. Он может быть успешно ис- пользован для изменения набора данных, который возвращается по умолчанию, — например, для фильтрации данных. В следующем примере возвращаются только имена, начинающиеся с буквы «В». Сервер доступа к данным: function TForml,ProviderlDataRequest(Sender; TObject; Input: OleVariant): OleVariant; begin with (Sender as TDataSetProvider) do begin DataSet.Filter := Input; DataSet.Filtered := True: DataSet.First: Result := Data; end; end: Клиент: procedure TForml.ButtonlClick(Sender: TObject): begin ClientDataSetl.Data := ClientDataSetl.DataRequest('Name' = 'B*'): end: Фильтрацию можно осуществлять во время выполнения без изменения свой- ства Active компонента-источника данных. И, наконец, метод Execute используется при выполнении запроса для компонен- тов TQuery или TStoredProc (либо аналогичных компонентов, использующих другие механизмы доступа к данным), расположенных на сервере доступа к данным. Как и. для описанного выше свойства CommandText, компонент TCIientDataSet должен ссылаться на удаленный компонент TDataSetProvider, который, в свою очередь, должен ссылаться на компонент TQuery или TStoredProc. При вызове метода Execute возможны два варианта. Первый вариант — значение свойства TCIientDataSet. CommandText непусто, и в свойстве Options компонента TDataSetSetProvider установ- лен флаг poAllowCommandText. В этом случае метод Execute в качестве SQL-запроса использует содержимое свойства CommandText, а значения свойства TCIientDataSet. Params посылаются в качестве параметров запроса. После его выполнения новые значения свойства Params становятся доступными в компоненте TCIientDataSet. Второй вариант — значение свойства TCIientDataSet.CommandText является пус- тым либо не установлен флаг poAllowCommandText свойства Options компонента TDataSetProvider. В этом случае в качестве запроса используется текущее содер- жимое компонента TQuery или TStoredProc. Как было сказано ранее, запретить выполнять те или иные операции редакти- рования в компоненте TCIientDataSet удобнее в серверном приложении, исполь- зуя соответствующие свойства компонента TDataSetProvider.
Исторический экскурс 583 Переход от Delphi 5 к Delphi 6 В свойстве Options компонента TDataSetProvider появился новый флаг — poRetainServerOrder. Когда он установлен, в клиентском приложении не должно происходить изменений в сортировке записей по сравнению с той, которая про- изводится на сервере доступа к данным. Кроме того, в секции protected появи- лись новые методы — FetchDetailsFromServer и GetDataSetFromDelta. Они не могут быть вызваны из экземпляра класса и служат для поддержки однонаправленных курсоров. Необходимость такой поддержки была вызвана появлением новой тех- нологии доступа к данным — dbExpress, в которой используются только однона- правленные курсоры. У компонента TClientDataSet появились новые свойства. is Connect! onBroker — ссылка на компонент TConnectionBroker, который поддерживает соединение с сервером доступа к данным. О самом компоненте TConnectionBroker будет рассказано далее. Я DisableStringTrim. При значении этого свойства, равном True, строковые поля будут передаваться «как есть», включая завершающие пробелы. При значе- нии этого свойства, равном False, пробелы в конце строки в клиентское при- ложение не передаются, и этим способом можно снизить сетевой трафик за счет уменьшения объема передаваемых строковых данных. Новые компоненты Delphi 6 для создания DataSnap-приложений Компонент TConnectionBroker Компонент TConnectionBroker используется в клиентском приложении. Этот компонент, который является дополнительным звеном между компонентами TClientDataSet и TXXXConnection, используется для централизации связи несколь- ких компонентов TClientDataSet с сервером доступа к данным (рис. 12.28). Delphi 5 Delphi 6 Рис. 12.28. Рекомендуемые модели клиентов DataSnap-приложений в различных версиях Delphi Модель распределенного приложения, используемая в Delphi 5, также оста- лась работоспособной: компонент TClientDataSet обладает свойством Connection, с помощью которого можно ссылаться на компонент TXXXConnection. Преимущества новой модели проявляются в том случае, когда, например, в кли- ентском приложении имеется несколько компонентов TXXXConnection и по каким- либо причинам (например, произошел отказ компьютера, на котором выполнялся
584 Глава 12. Технология DataSnap сервер) необходимо использовать другой компонент TXXXConnection. Для модели, реализованной в Delphi 5, для каждого компонента TClientDataSet необходимо изме- нить свойство Connection, а для модели, рекомендуемой к использованию в Delphi 6, достаточно изменить одно свойство Connection компонента TConnectionBroker. Свойства, события и методы компонента TConnectionBroker совпадают со свой- ствами, событиями и методами компонента TXXXConnection и не вызывают затруд- нений при их использовании. Компонент TSharedConnection Компонент TSharedConnection используется в клиентском приложении. Он необ- ходим в тех случаях, когда сервер доступа к данным состоит из нескольких ком- понентов TRemoteDataModul е. Наличие нескольких модулей данных часто упрощает архитектуру сервера. Однако для обращения к дочерним модулям следует созда- вать код, подчиненный определенным правилам. Ранее уже был рассмотрен при- мер создания и работы сервера с дочерними модулями. Ключевым у компонента TSharedConnection является свойство ChildName, кото- рое ссылается на дочерний модуль. В свойстве Parentconnection этого компонента необходимо сослаться на компонент TXXXConnecti on, который обеспечивает транс- портировку данных. Использование остальных свойств и обработчиков событий не вызывает затруднений. Компонент TLocalConnection Этот компонент, хотя и помещен на страницу DataSnap, никакого отношения к мно- гозвенным приложениям не имеет. Типичный проект с использованием этого компонента выглядит следующим образом (рис. 12.29). Рис. 12.29. Использование компонента TLocalConnection в проектах создания баз данных Новый универсальный механизм доступа к данным dbExpress, предложенный Borland, позволяет создавать только однонаправленный курсор. Это означает, что клиентское приложение может просматривать записи в наборе данных от первой к последней, но не наоборот. Для того чтобы пользователь мог передвигаться по за- писям произвольно, они кэшируются с использованием компонента TClientDataSet. Для работы с компонентом TClientDataSet требуется компонент TDataSetProvider.
Реализация DataSnap-серверов как сервисов Windows NT/2000 585 В большинстве случаев его достаточно. Однако если нужно обратиться к некото- рым скрытым в секции protected методам компонента TDataSetProvider, необхо- дим компонент TLocalConnection. У этого компонента имеется ключевое свойство AppServer, доступное во время выполнения. Это свойство содержит ссылку на ин- терфейс lAppServer, используя который можно обратится к некоторым защищен- ным (protected) методам компонента TDataSetProvider. Переход от Delphi 6 к Delphi 7 Как было отмечено ранее, изменения в технологии DataSnap, связанные с выходом Delphi 7, были незначительны. Следует отметить ряд новых возможностей, связан- ных с использованием протокола SOAP (Simple Object Access Protocol) и пред- ставлением серверов доступа к данным в виде web-сервисов XML. В частности, к DataSnap-серверам, имеющим несколько модулей данных, теперь можно доба- вить специальный модуль данных SOAP. Кроме того, с помощью компонента TSOAPConnection можно обращаться к расширениям интерфейсов серверов прило- жений. Отметим, однако, что тема создания и применения web-сервисов сама по себе достаточно объемна и выходит за рамкп темы данной книги. Отметим также, что реализация DataSnap-приложений может быть осуществлена различными способами. В частности, сервер доступа к данным может быть реализован в виде сервисов операционной системы. Этому вопросу посвящен следующий раздел. Реализация DataSnap-серверов как сервисов Windows NT/2000 Реализация сервера доступа к данным в виде сервиса операционной системы по- зволяет работать с сервером, когда на содержащем его компьютере не открыто ни одного сеанса (не зарегистрировано ни одного пользователя). Именно в таком ре- жиме работают серверы баз данных, и представляется разумным создавать серверы доступа к данным, поддерживающие этот режим. Доступ к сервисам Windows NT и Windows 2000 осуществляется через ме- неджер управления сервисами (Service Control Manager) — соответствующий значок имеется в разделе Administrative Tools панели управления Windows. Сер- вис операционной системы может запускаться автоматически при ее загрузке. При этом следует учитывать, что сервис может взаимодействовать с другими сервисами. Сервисы, которые требуются для работы данного сервиса, называются подчиненными, и их необходимо указывать, описывая параметры запуска вновь создаваемого сервиса. Кроме того, сервис должен откликаться на ряд команд, доступных в средстве управления сервисами, такие как Start, Stop, Pause, Resume, Restart. Данные о сервисе, такие как путь к приложению, название сервиса, список подчиненных сервисов и групп сервисов, заносятся в системный реестр. Соот- ветственно, приложение должно поддерживать регистрацию сервиса, равно как и удаление данных из реестра. И, конечно же, создаваемое приложение должно иметь библиотеку типов и механизмы регистрации ее в системном реестре, как это делается для DataSnap-серверов.
586 Глава 12. Технология DataSnap В Delphi 7 имеется мастер создания сервисов Windows NT и Windows 2000. Для его запуска необходимо выбрать в главном меню команду File ► New ► Other и на странице New окна репозитария объектов выбрать значок Service Application. После этого мастером будут созданы главный модуль приложения и модуль, содержащий класс-потомок TService. Код главного модуля приложения весьма напоминает код обычного прило- жения: Application.Initialize: Application.CreateFormCTServicel, Servicel): Application.Run: Но при кажущей внешней схожести этого кода и кода обычного приложения объект TApplication и переменная Application отличаются от таковых для обыч- ных приложений. Поэтому ни в коем случае нельзя создавать в сервисных при- ложениях новую форму выбором команды File ► New Form. Как только в главном модуле появится ссылка на модуль Forms, произойдет ошибка в определении классов, и создаваемое приложение не сможет работать как сервис операцион- ной системы. Модуль, в котором следует реализовывать класс-потомок TService, содержит специальную форму, которая напоминает модуль данных — TDataModule. На нее можно помещать невизуальные компоненты. Прежде всего, следует переопреде- лить свойство Di spl ayName — именно это вновь определяемое имя и будет показано в окне менеджера управления сервисами (рис. 12.30). Рис. 12.30. Имя вновь созданного сервиса, показанное в окне менеджера управления сервисами Значения остальных свойств на данном этапе оставим равными значениям по умолчанию. Далее необходимо реализовать процедуру выполнения сервиса. Это делается путем создания обработчика события OnExecute класса-потомка TService:
Реализация DataSnap-серверов как сервисов Windows NT/2000 587 procedure TServicel.ServiceExecute(Sender: TService): begin repeat ServiceTh read.ProcessRequests(False); until FIsStopped: end; Эта процедура вызывается автоматически при старте сервиса, ее завершение означает прекращение работы сервиса и переход его в состояние Stopped. Поэтому она представляет собой бесконечный цикл, который прерывается только тогда, когда значение логической переменной FIsStopped становится равным True. Эту переменную определим в секции interface модуля реализации TSerwicel (гло- бальная переменная), а ее значение будем менять в обработчике события OnStop класса TServicel, который пока определим следующим образом; procedure TServicel.ServiceStop(Sender; TService; var Stopped: Boolean): begin Stopped := True: FIsStopped ;= Stopped; end; Вернемся еще раз к обработчику события OnExecute. К одному сервису опера- ционной системы может одновременно обращаться несколько клиентов, для ка- ждого из них создается свой поток выполнения. Этот поток вызывает события OnStart и OnExecute. При описании события OnExecute в виде бесконечного цикла необходимо предусмотреть механизм получения и обработки сообщений из оче- реди — иначе, например, нельзя будет остановить сервис или поставить его в со- стояние ожидания с помощью менеджера управления сервисами. Это достигается вызовом метода ProcessRequest класса TServiceThread. Ссылка на экземпляр этого класса находится в свойстве ServiceThread, которое определено в классе TService. Вместо экземпляра TServiceThread можно создать собственный поток выполне- ния, но в данном проекте это роли не играет. И, наконец, необходимо также создать обработчик события OnStart, в котором просто изменить значение параметра метода Started: procedure TServicel.ServiceStart(Sender: TService: var Started: Boolean); {Необходимо создать обработчик этого события, иначе автоматический старт после перезагрузки компьютера будет невозможен} begin FIsStopped := False; Started := True; end: Без этого обработчика сервис не будет автоматически запускаться после за- грузки операционной системы.
588 Глава 12. Технология DataSnap В принципе сервис готов — пока без DataSnap-части. Но его можно уже устано- вить и проверить работоспособность. Для установки сервиса необходимо скомпи- лировать данный проект и вызвать его из командной строки (Start ► Run) с клю- чом /install (рис. 12.31). Рис. 12.31. Регистрация сервиса После успешного выполнения команды операционная система выводит на экран сообщение об успешной установке сервиса (рис. 12.32). lofurrndtkjn Service installed successful ш Рис. 12.32. Сообщение об успешной установке сервиса Установка не означает, что сервис будет запускаться автоматически — для за- пуска нужно открыть окно менеджера управления сервисами, выделить строку с именем созданного сервера и щелкнуть на кнопке Start панели инструментов. Есть и другой способ: если свойство StartType компонента Servicel оставлено по умолчанию равным stAuto, можно просто перезагрузить операционную систему. Теперь добавим к проекту часть, отвечающую за сервер доступа к данным. Выберем команду File ► New ► Other и на странице Multitier окна репозитария объек- тов выберем значок Remote Data Module. Имя класса в появляющемся диалоговом окне создания удаленного модуля данных определим как Test, в раскрывающемся списке Instancing выберем пункт Multiple Instance. Выбор этого пункта в данном случае является обязательным, поскольку второй (и тем более третий, четвер- тый...) экземпляр сервиса не может быть запущен в принципе. Назначение рас- крывающегося списка Threading Model объяснялось ранее (см. главу 6). Чтобы код был проще, выберем в этом списке пункт Apartament. На появившийся кон- тейнер — потомок класса TRemoteDataModule (имя вновь созданного класса — TTest, если диалоговое окно было заполнено так, как рекомендовано) — поместим компо- ненты TSession, TDatabase, ТТаЫе и TDataSetProvider. Свойство AutoSessionName компо- нента TSession установим равным True; свойство AliasName компонента TDatabase —
Реализация DataSnap-серверов как сервисов Windows NT/2000 589 равным DBDEMOS; свойство DatabaseName компонента TDatabase — равным МуОВ; свой- ство DatabaseName компонента ТТаЫе — равным МуОВ; свойство TableName компонента ТТаЫе — равным customer, db, а в свойстве DataSet компонента TDataSetProvider сошлемся на компонент Tablel. Простейшее серверное DataSnap-приложение, вы- полненное в виде сервиса операционной системы, уже готово. При этом имейте в виду следующее. Если в Windows NT не установлен пакет обновления Service- Pack 6, а сервис настроен на автоматический запуск при перезагрузке операци- онной системы, то сервис стартовать не сможет. При перезагрузке появляется диагностическое сообщение, показанное на рис. 12.33. Рис. 12.33. Сообщение о невозможности запуска сервиса Это происходит потому, что Windows NT пытается запустить данный DataSnap- сервис раньше, чем стартовал сервис RpcSS, который обеспечивает возможность удаленного вызова процедур. Чтобы избавиться от вывода этого диагностического сообщения, необходимо в редакторе свойства Dependencies компонента Servicel (потомка TService) добавить новую зависимость. Затем ее следует выделить, и в ин- спекторе объектов появятся свойства зависимых сервисов. Далее нужно раскрыть список свойства Name и выбрать в нем сервис RpcSS. Все данные о зависимостях между сервисами содержатся в реестре, и именно поэтому необходимо выполнить повторную регистрацию сервиса — если этого не сделать, все описанные зависимо- сти не будут учтены операционной системой. Для повторной регистрации необхо- димо запустить созданное приложение из командной строки с ключом /unistall (иначе операционная система сообщит о том, что сервис уже существует), а за- тем запустить его еще раз, но уже с ключом /install. После внесенных измене- ний DataSnap-сервис будет стартовать при загрузке компьютера автоматически. Данный проект некорректно себя ведет при попытке остановить сервис в тот момент, когда с ним соединены клиенты. Сервис останавливается, но при этом начинается вывод диагностических сообщений об ошибках. Простейшее реше- ние — запретить остановку сервиса при наличии соединенных с ним клиентов. Для этого введем глобальную переменную MyClassList:TThreadList и при созда- нии нового экземпляра — потомка класса TRemoteDataModule — будем добавлять ссылку на экземпляр в список, а при разрушении — удалять. Для этого перепи- шем его виртуальные методы AfterConstruction и BeforeDestruction. Соответственно, при попытке остановки сервиса будем проверять, является ли список клиентов пустым. Сам экземпляр MyClassList будем создавать в момент загрузки приложения (секция initialization) и разрушать его перед остановкой (секция finalization). Изменения в обработчике события OnStop класса TServicel имеют вид:
590 Глава 12. Технология DataSnap procedure TServicel.ServiceStopISender: TService; var Stopped: Boolean); var U: TList; begin Stopped ;= True: if Assigned(MyClassList) then try L:=MyClassLi st.LockLi st; if L.Count>0 then begin MessageDlgl'COM object(s) are still active'. mtlnformation. [mbOK], 0); Stopped := False; end; finally MyClassLi st.Uni ockLi st: end; FIsStopped := Stopped; end; Соответственно, перекрытые методы BeforeDestruction и AfterConstruction модуля с реализацией TTest (потомок TRemoteDataModulе) выглядят следующим образом; procedure TTest.BeforeDestructi on; var N: Integer: L: TList; begin if Assigned(MyClassList) then try L := MyClassList.LockList: N := L.IndexOf(Self): if N >= 0 then L.DeleteW; finally MyClassLi st.UniockLi st: end; inherited BeforeDestruction; end; procedure TTest.AfterConstructi on; var L: TList: begin inherited AfterConstruction: if Servicel.IsStopped then raise Except!on.Create!'Service not started’): if Assigned(MyClassList) then try L := MyClassList.LockList: L.Add(Self): finally
Реализация DataSnap-серверов как сервисов Windows NT/2000 591 МуClassLIst.Unlос k Li st; end: end: initialization TComponentFactory.CreatelComServer. TTest, Classjest. ciMultiInstance, tmApartment): MyClassList := TThreadList.Create: finalization MyClassList.Free: end. Чтобы не было ошибок при компиляции, необходимо в этих модулях со- слаться друг на друга — определить имя другого модуля в разделе uses секции implementation. Обращаем также внимание на генерацию исключения в конструк- торе — исключение будет возбуждаться при попытке соединения клиента с сер- висом, если сервис не запущен. В принципе, на этом создание простейшего DataSnap-сервиса можно считать завершенным. Дальнейшее развитие данного проекта — управление сервисом. Администратору может быть интересно, какие клиенты находятся в данный мо- мент в соединении с сервисом. Кроме того, клиенты могут посылать сообщения администратору, которые следует вывести на экран. Далее, DataSnap-сервис мо- жет иметь ряд параметров, для установки которых также может быть желательно использование визуального интерфейса. Таким образом, следующую задачу можно сформулировать как добавление визуальной формы к DataSnap-сервису. При старте сервис не должен сразу же показывать форму — ее отображение должно инициироваться отдельной командой. Обычно для этого помещают не- большие значки в правом углу панели задач. Так реализован, например, сервер Borland Socket Server (scktsrvr.exe), который используется в технологии DataSnap для передачи данных по протоколу TCP/IP и может работать и как приложение, и как сервис операционной системы. Реализуем отображение формы в нашем сервисе через значок в правой части панели задач. Для этого выберем в главном меню команду File ► New Form, сохра- ним модуль с формой под каким-либо осмысленным именем и после этого не- медленно удалим форму из проекта с помощью команды Project ► Remove From Project. Далее просто будем ссылаться на данный модуль без включения его про- ект. Следует убедиться, что ссылка на модуль Forms не попала в код проекта — иначе возможны конфликты различающихся объектов TAppl ication. Форму будем создавать сразу же после старта приложения — для этого в код файла проекта добавим команду: if Forml=nil then Application.CreateFormlTForml, Forml); Во вновь созданной форме разместим значок для панели задач. Для этого соз- дадим новый файл ресурсов с расширением *.res, воспользовавшись прилагае- мым к Delphi редактором ресурсов, а в созданный файл ресурсов поместим зна- чок, размеры которого сделаем равными 16x16 (значки другого размера не могут
592 Глава 12. Технология DataSnap находиться на панели задач). Создадим отдельный модуль, куда поместим вспо- могательные функции для регистрации, модификации и удаления значков. В нем надо сослаться на модуль Shell API: function Task Ba rAddIcon(hWindow:THandle: ID: Cardinal: ICON: hlcon: CallbackMessage: Cardinal: Tip: String): Boolean: var NID: TNotifylconData: begin Ti11CharINID, SizeOf(TNotifylconData). 0); with NID do begin cbSize := SizeOf(TNotifylconData): Wnd := hWindow: uID := ID: uFlags := NIF_MESSAGE or NIFJCON or NIF_TIP; uCallbackMessage := CallbackMessage: hlcon := Icon; if Length(Tip) > 63 then SetLength(Tip, 63): StrPCopy(szTip, Tip): end: Result := Shell_NotifyIcon(NIM_ADD. @NID): end: В приведенном фрагменте кода указывается дескриптор окна, которому сле- дует посылать сообщения, — в данном случае окна формы. Поле FNID.uID содер- жит идентификатор значка и используется при отображении нескольких знач- ков. Поле FNID.uCallbackMessage содержит сообщение, которое будет посылаться окну FNID.Wnd при наведении указателя мыши на значок или при щелчке на кнопке мыши, когда указатель находится на значке. Поле FNID.szTip содержит текст под- сказки, которая будет всплывать при наведении указателя мыши на значок. Поле uFlags определяет режим работы приложения при щелчке на значке, в данном случае осуществляются посылка сообщений окну FNID.Wnd, вывод значка, вывод всплывающей подсказки. И наконец, вызов метода Shell_NotifyIcon использует за- полненную структуру для вывода значка. function TaskBarDeleteIcon(hWindow: THandle: ID: Integer): Boolean: var NID: TNotifylconData: begin Fi11Char(NID. SizeOf(TNotifylconData), 0): with NID do begin cbSize := SizeOf(TNotifylconData): Wnd := hWindow: uID := ID: end: Result := Shell_NotifyIcon(NIM_DELETE, @NID): end:
Реализация DataSnap-серверов как сервисов Windows NT/2000 593 Для удаления значка с панели задач заполняем ту же структуру и вызываем метод Shell_Not1fyIcon с параметром NIM_DELETE. Теперь зарегистрируем значок. При создании соответствующей процедуры необходимо учитывать, что регистрация в операционной системе другого поль- зователя вызывает разрушение и повторное создание панели задач. При этом сервис заново не стартует, и если однократно зарегистрировать значок в момент старта сервиса, то он исчезнет с панели задач после регистрации другого пользо- вателя. Поэтому требуется получать от оболочки Windows нотификационные сообщения о создании панели задач. Начиная с Internet Explorer версии 4.0, при создании панели задач всем окнам рассылается сообщение. Этому сообщению не соответствует ни одна из констант среди WM_XXX, но ее численное значение можно получить во время выполнения, если вызвать метод RegisterWindowMessage с пара- метром TaskbarCreated. Поскольку численное значение получается только во время выполнения, то мы не можем создать обработчик события Windows традицион- ным способом — необходимо создавать и регистрировать новую процедуру для обработки сообщений на форме. Объявим глобальную переменную TaskbarRestart, куда будем помещать резуль- тат вызова метода RegisterWindowMessage. В модуле, связанном с формой, объявим в секции private переменную FHI :Т1соп. Также объявим две переменные FNewProc, FOldProc:pointer класса. В первой из них будет храниться адрес созданного нами обработчика событий, а вторая (указатель) будет ссылаться на обработчик собы- тий по умолчанию. Для создания нового обработчика событий реализуем метод: procedure TForml.HookProc(var Message: TMessage): begi n if Message.Msg = TaskbarRestart then TaskBarAddIcon(Handle. 0. FHI.Handle, WM_ICONNOTIFY, 'Midas service'): Message.Result := CallWindowProc(FOldProc. Handle, Message.Msg. Message.wParam, Message.LParam): end: Здесь при получении зарегистрированного методом RegisterWindowMessage со- общения вызывается описанный ранее метод TaskBarAddlcon. После этого вызы- вается обработчик сообщений формы по умолчанию, адрес которого хранится в переменной FOldProc. Конструктор класса TForml перепишем следующим образом: constructor TForml.Create(AOwner:TComponent): begin inherited: FHI := TIcon.Create: FHI.Handle := LoadIcon(HInstance, 'MYICON'); TaskBarAddIcon(Handle. 0. FHI.Handle. WMJCONNOTIFY, 'Midas service'): TaskbarRestart := RegisterWindowMessage!'TaskbarCreated'):
594 Глава 12. Технология DataSnap FNewProc := MakeObjectlnstance(HookProc): FOldProc := Pointer(GetWindowLong(Handle, GWL_WNDPROO); SetWindowLong(Handle, GWL_WNDPROC. Integer(FNewProc)): end; В приведенном фрагменте кода в конструкторе создается объект TIcon и из ресурсов загружается значок. После этого вызывается созданный ранее метод TaskBarAddlcon, в котором заполняется структура TNotifylconData. Далее, получаем численное значение нотификационного сообщения о создании панели задач и со- храняем значение в TaskbarRestart. Метод MakeObjectlnstance в памяти компьютера создает копию кода, реализованного в HookProc, и возвращает адрес в переменную FNewProc. Адрес обработчика событий по умолчанию получаем вызовом метода GetWindowLong. И наконец, ссылаемся на новый обработчик события командой SetWindowLong. Параметр WM_ICONNOTIFV, используемый в методе TaskBarAddlcon, оп- ределен как константа: const WMJCONNOTIFY = WMJJSER + 1234; Для работы контекстного меню поместим компонент TPopupMenu на форму. Определим метод Stop — остановка сервиса. В обработчике события OnCl 1 ck для команды Stop поместим код остановки сервиса: procedure TForml.StoplCl1ck(Sender: TObject): var Test: Boolean: begin Servicel.ServiceStop(nil, Test); end: Аналогично, добавим команду Properties в контекстное меню и определим об- работчик события: procedure TForml.PropertieslC11ck(Sender: TObject): begin Show: BringWindowToTop(Handle): repeat Appl 1cati on.Handl eMessage: until FTerminated: FTerminated := False: end: Но этого еще недостаточно — осталось обеспечить вызов контекстного меню при щелчке правой кнопки мыши на значке. Сообщение WM ICONNOTIFY будет по- сылаться форме. Поэтому создаем обработчик сообщения на форме: procedure TForml.WMIconNotify(var Message: TMessage): var P: TPoint:
Реализация DataSnap-серверов как сервисов Windows NT/2000 595 begin if Message.IParam = WM_LBUTTONDOWN then begin Show: BringWindowToTop(Handle): repeat Appli cati on.Handl eMessage: until FTerminated: FTerminated := False: end: if Message.IParam = WM_RBUTTONDOWN then begin GetCursorPos(P); PopupMenul.Popup(P.X. P.Y); end: end: При щелчке левой кнопкой мыши на значке будет сразу же появляться форма. При щелчке правой кнопкой мыши будет вызываться контекстное меню. Перед остановкой сервиса необходимо удалить созданный значок, а также освободить ресурсы, занятые у операционной системы методом MakeObjectlnstance. Для этого необходимо переписать деструктор класса TForml, где вызовем создан- ный ранее метод TaskBarDeletelcon: destructor TForml.Destroy: begi n TaskbarDeleteIcon(Handle, 0): FHI.Free: SetWindowl_ong(Handle. GWL_WNDPROC, Integer(FOldProc)): FreeObjectlnstance(FNewProc): inherited Destroy; end: Обратите внимание на необходимость вновь сослаться на обработчик собы- тий по умолчанию — иначе программа завершится с кодом ошибки. Вызов функ- ции TaskbarDeletelcon в деструкторе требуется для того, чтобы после остановки сервиса значок исчезал с панели задач. В противном случае при следующем старте сервиса появится еще один значок и т. д. Данного кода вполне достаточно для отображения формы сервиса. Осталось заполнить форму информационными элементами управления. Поместим компо- нент TListBox на форму и будем в него заносить информацию о пользователях, работающих в данный момент с сервисом. В предыдущих версиях Windows NT (включая и версии с установленным пакетом Service Pack 5) это можно было сделать, переписав методы AfterConstruction и BeforeDestruction класса TTest, где прямо в список можно было вывести текстовую информацию. В последних вер- сиях вывод всей графической информации на экран автоматически осуществля- ется из главного потока выполнения — и это разумно, так как в этом случае не появляются дефекты графики. С другой стороны, COM-объект создается в допол- нительном потоке, и пока не отработает его конструктор, главный поток будет
596 Глава 12. Технология DataSnap находиться в состоянии ожидания. Если из метода конструктора обратиться к вы- воду графики на экран, то дополнительный поток будет тоже ожидать, когда освободится главный. В результате каждый из потоков будет ждать, когда другой поток выполнит свои методы, то есть произойдет взаимная блокировка, проявляю- щаяся в «зависании» приложения. Необходима асинхронная развязка, и наибо- лее простой способ ее создания — использование компонента TTimer. Поместим его на форму и создадим обработчик события OnTimer: procedure TForml.TimerlTimer(Sender: TObject): var L: TList: I: Integer: S: String: T: TTest: begin Li stBoxl.Items.Cl ear: try L := MyClassList.LockList; for I := 0 to L.Count - 1 do begin T := TTest(L[I]): S := DateTimeToStr(T.LoginDate); ListBoxl.Items.AddObject(S. L[I]); end: finally MyClassLi st.UniockLi st; end: end: Можно также поместить кнопку для прерывания соединения клиента с DataSnap- сервисом: procedure TForml.ButtonlClick(Sender: TObject); var N: Integer: UD: TTest: L: TList; begin N := ListBoxl.Itemindex; if N >= 0 then begin UD := TTest(ListBoxl.Items.Objects[N]): if Assigned(UD) then try L := MyClassList.LockList: if L.IndexOf(UD) > 0 then UD.Free; finally MyClassLi st.UniockLi st; end: end; end:
Заключение 597 Разрушение объекта следует выполнять в защищенном блоке, поскольку раз- рыв связи может быть до этого инициализирован клиентом. После старта сервиса панель задач должна выглядеть так, как показано на рис. 12.34. ДЖ ®РМ Рис. 12.34. Панель задач со значком сервиса (обведен) При щелчке левой кнопки мыши на значке появляется форма, показанная на рис. 12.35. Рис. 12.35. Работа СОМ-сервера, реализованного в виде сервиса Windows NT В данной форме в качестве идентификаторов пользователей используются указатели на объекты TTest, но в реальных приложениях в список можно поме- щать данные метода Log 1 п. Таким образом, в реальных приложениях форма мо- жет быть использована, например, для ведения списка пользователей и их паро- лей, для определения прав доступа конкретных пользователей к экспонируемым данным, для приема сообщений от клиента и многих других полезных вещей — все зависит от фантазии разработчиков приложений. Заключение В данной главе мы обсудили вопросы создания многозвенных информацион- ных систем с помощью технологии DataSnap, ранее носившей название MIDAS (Multi-tier Distributed Application Service Suite) и предназначенной для создания и последующей эксплуатации удаленных серверов автоматизации, предоставляю- щих своим контроллерам доступ к данным серверных СУБД. Мы рассмотрели составные части приложения в архитектуре «клиент-сер- вер», обсудили типичные проблемы поставки, эксплуатации и сопровождения информационных систем, убедились, что при использовании стандартных архи- тектур создания многопользовательских информационных систем можно столк- нуться с серьезными проблемами, требующими материальных затрат. Мы также узнали, что многие из этих проблем решаются путем создания сервисов про-
598 Глава 12. Технология DataSnap межуточного звена, например серверов доступа к данным, в том числе с помо- щью технологии DataSnap. Мы обсудили, что представляет собой DataSnap. Мы узнали, что: Я DataSnap представляет собой технологию создания распределенных систем, состоящих их сервера баз данных, сервера доступа к данным (который, в свою очередь, является клиентом сервера баз данных) и так называемого тонкого, или облегченного, клиентского приложения, являющегося клиентом сервера доступа к данным; Я DataSnap-сервер доступа к данным представляет собой сервер автоматизации; Я с технологической точки зрения DataSnap есть реализованная в ряде компо- нентов VCL надстройка над СОМ, обеспечивающая превращение набора дан- ных в допустимый для СОМ тип, передачу таких данных обычным для СОМ способом и обратное восстановление набора данных на стороне, эти данные получающей. Мы обсудили создание серверной и клиентской частей простейшего DataSnap- приложения, применение модели briefcase для работы в режиме отсутствия со- единения с сервером доступа к данным, а также корректную организацию много- пользовательской обработки данных в распределенных системах. Мы также нау- чились создавать клиентские приложения в виде активных форм, поставлять их через Интернет и решать возникающие при этом технические проблемы. Изучив основы создания DataSnap-приложений, мы рассмотрели и некото- рые дополнительные возможности, предоставляемые этой технологией, такие как работа со связанными таблицами, выполнение пользовательских запросов, использование нескольких модулей данных в сервере доступа к данным, обраще- ние к компонентам VCL из кода удаленных модулей данных. Мы также обсудили способы повышения интерактивности DataSnap-приложений, такие как перенос бизнес-правил в клиентское приложение, организацию сортировки данных в кли- ентском приложении. Кроме того, мы рассмотрели несколько примеров создания дополнительных методов удаленных модулей данных, в частности, для аутенти- фикации пользователя и передачи текстовых сообщений от клиента к серверу доступа к данным. Мы также изучили вопросы реализации нотификационных сообщений с помощью компонента TSocketConnection и познакомились с некото- рыми примерами применения технологии DataSnap при создании однозвенных приложений, использующих клиентские наборы данных в качестве альтернативы обычным базам данных. Учитывая, что технология DataSnap за время своего существования претер- пела многочисленные изменения, мы обсудили все нововведения, появившиеся в ее реализации с момента выхода Delphi 4. В заключение мы рассмотрели реализацию DataSnap-сервера в качестве сер- виса операционной системы. Мы обсудили особенности сервисов Windows NT и Windows 2000, такие как возможность автоматического старта при загрузке операционной системы, наличие зависимостей от старта других сервисов, отсут-
Заключение 599 ствие требования регистрации пользователя за данным компьютером. Мы узнали, как пользоваться мастером Delphi для создания сервисов Windows NT и как создать DataSnap-часть такого сервиса, а также обсудили процедуру создания визуального интерфейса DataSnap-сервиса, в частности особенности отображе- ния значка на панели задач, создания контекстного меню и отображения форм. Технология DataSnap, будучи довольно популярной, отнюдь не является единственной технологией распределенных вычислений, основанных на приме- нении СОМ. Следующая глава будет посвящена другому типу распределенных приложений, основанному на применении технологии Active Server Pages и соз- дании серверных ASP-объектов в Delphi.
ГЛАВА 13 Создание ASP-объектов Технология ASP (Active Server Pages — активные серверные страницы) позволяет динамически формировать web-страницы, код которых выполняется web-серве- ром. Вместе с Microsoft Internet Information Sever поставляется несколько СОМ- объектов, выполняющих серверный код. Современная версия этой технологии позволяет использовать в ASP-страницах произвольные серверные компоненты. Созданию таких компонентов и посвящена данная глава. Клиентское приложение, использующее ASP-объекты, представляет собой HTML-документ, который может также включать клиентский и серверный код на языках сценариев. Этот документ в принципе можно прочесть с помощью лю- бого web-браузера. Обычно эти HTML-документы размещаются на каком-либо web-сервере (как правило, это Microsoft Internet Information Server версии 3.0 и выше). Web-сервер, получив требование о предоставлении документа, считывает его из локального хранилища и передает клиенту, при этом часть информации вносится в документ web-сервером динамически; сам же web-сервер при этом может обращаться к ASP-объектам (входящим в комплект поставки Internet Information Server или созданным сторонними разработчиками). Обычно web- документы, содержащие обращения к ASP-объектам, имеют расширение *.asp. Примеры подобных документов можно найти в каталогах, создаваемых при установке Internet Information Server. Иерархия ASP-объектов Иерархию объектов, используемых в технологии ASP, иллюстрирует рис. 13.1. Объект Request Объект Request используется для доступа к данным, которые формируются клиен- том при обращении к ASP-серверу. Доступ к объекту Request получают с помощью интерфейса I Request, основные свойства и методы которого перечислены ниже. Свойство 01 ientSerti ficate содержит значения всех полей клиентского серти- фиката, которые пересылаются в НТТР-сообщении. S Свойство Cookies содержит значения всех заголовков файлов cookie в НТТР- запросе. Файлы cookie — небольшие текстовые файлы, которые запоминаются на компьютере конечного пользователя и передаются на сервер в каждом запросе. С их помощью можно создать «сеанс» для клиента. В этом сеансе клиент обращается первоначально к одной, затем к другой странице и т. д., причем клиент не имеет права перейти к следующей странице, минуя предыдущую.
Иерархия ASP-объектов 601 Рис. 13.1. Иерархия объектов ASP-сервера И Свойство Form позволяет получить значения заполненных полей в форме при обращении клиента к ASP-серверу с помощью формы. Для этого в коде вы- полняется обращение к свойству Form с добавлением имени поля. Ж Свойство ServerVariables содержит большинство переменных, необходимых для формирования НТТР-заголовков. Я Свойство Total Bytes содержит суммарное число байтов, которые передаются клиентом на сервер. Реальное число байтов может быть меньше, так как прото- кол HTTP не позволяет пересылать пакеты, размер которых превышает 8 Кбайт. Ж Свойство Querystring содержит имена и значения полей запроса при исполь- зовании метода GET. Ss Свойство Body содержит имена и значения полей запроса при использовании метода POST. я Метод BinaryRead используется для получения содержимого всего запроса, по- сылаемого клиентом, если размер запроса превышает 8 Кбайт. Вызов этого метода позволяет получить следующие 8 Кбайт данных HTTP-запроса. Вы- зов следует повторять до тех пор, пока не будет прочитан весь запрос, при этом размер последнего пакета может быть менее 8 Кбайт. Результаты вызова метода запоминаются в двоичном массиве. Следует обратить внимание на то, что после вызова этого метода нельзя обращаться к свойству Form. Объект Response В объекте Response формируется отклик ASP-сервера, который передается клиенту. Доступ к этому объекту осуществляется через интерфейс IResponse. Основные свойства и методы этого интерфейса перечислены ниже. Я Свойство Cook I es позволяет создать коллекцию параметров вместе с их значе- ниями. К этой коллекции также добавляется время, в течение которого эту коллекцию следует сохранять в клиентском приложении. При успешном по- лучении отклика клиент запоминает параметры на диске в виде файла. При
602 Глава 13. Создание ASP-объектов следующем обращении к серверу эти данные передаются в запросе и их можно получить с помощью свойства Request.Cookie. Анализ этих данных позволяет определить, обращался ли клиент к серверу ранее, и если обращался, то с каки- ми запросами. Файлы cookie используются для создания «сеанса» с клиентом в CGI-приложениях, поскольку IP-адреса клиента могут изменяться Proxy- серверами во время сеанса. В ASP-приложениях допускается также создание сеанса для клиента (это мы обсудим ниже). Я Если значение свойства Buffer равно True, то осуществляется буферизация от- клика. При этом отклик клиенту не отправляется до тех пор, пока не будет вызван метод Request. End (этот метод автоматически вызывается после обра- ботки всего ASP-документа, когда сформированы отклики от всех ASP-серве- ров) или Flush. Ж Свойство CacheControl определяет, может ли клиент кэшировать отклик. S Свойство Charset определяет имя шрифта для содержимого отклика. в Свойство ContentType определяет содержимое отклика. По умолчанию это свойство имеет значение text/html. Это значение необходимо изменять, на- пример, при передаче изображений (image/jpeg или image/gif и т. д.). ж Свойство Expi res при значении CacheControl =True определяет, сколько времени хранится отклик на клиентском месте. Я Свойство ExpiresAbsolute эквивалентно свойству Expires, но определяет абсо- лютную дату и время хранения отклика. Я Свойство IsClientConnected указывает, был ли клиент отсоединен от сервера. И Свойство Status определяет состояние отклика. Нормальное значение состоя- ния 200 ОК означает успешную генерацию отклика. При невозможности создать отклик следует изменить значение этого свойства. Так, следующее значение говорит, что пользователю запрещено обращаться к данной странице по со- ображениям безопасности (например, из-за неправильно введенного пароля): 401 Unauthorized ® Метод AddHeader добавляет заголовок в отклик в следующем виде: <ИМЯ ПАРАМЕТРА>=’’<ЗНАЧЕНИЕ>" И Метод AppendToLog добавляет строку в файл журнала web-сервера. Эта строка не передается клиенту. И Метод Bi naryWrite формирует двоичный отклик. Чаще всего используется для передачи клиенту изображений. Ж Метод Clear полностью очищает отклик. После этого его требуется формиро- вать заново. И Метод End прекращает обработку ASP-документа и немедленно возвращает результат клиенту. Вызывается автоматически после завершения обработки ASP-документа, но может быть вызван явно из кода ASP-сервера. После его вызова вызовы методов Write или BinaryWrite запрещены.
Иерархия ASP-объектов 603 Я Метод Fl ush отправляет текущее содержимое буфера клиенту. Формирование отклика может продолжаться дальше. И Метод Write используется для записи текстовой информации в отклик. Метод Redirect позволяет переадресовать запрос на другой URL-адрес. При этом можно поменять параметры запроса в объекте Request. Объект Server Объект Server позволяет обращаться к серверу Internet Information Server и экс- понирует ряд его методов и свойств. S Свойство ScriptTimeout — это время в минутах, в течение которого существует сеанс без генерации нового запроса или без вызова команды Refresh со стороны клиента. Я Вызов метода CreateObject используется для запуска СОМ-сервера, который будет принимать сценарии с ASP-документа и генерировать отклик, встав- ляемый вместо сценария. » Метод Execute выполняет сценарий в указанном ASP-файле. Ж Метод GetLastError возвращает объект ASPError, в котором можно получить полную информацию о последней ошибке. Метод HTMLEncode заменяет зарезервированные символы в HTML-документе подходящим набором символов, которые интерпретируются браузером и позво- ляют пользователю видеть на экране зарезервированные символы. Напри- мер, пусть дизайнер web-сайта хочет, чтобы пользователь на экране увидел сочетание символов <BR>. Если эту последовательность вставить в HTML-доку- мент, то она будет интерпретирована как разрыв строки. Для этой последова- тельности символов метод HTMLEncode вернет последовательность %3CBR%3E, при помещении которой в HTML-документ пользователь увидит на экране исход- ные символы <BR>. Я Метод MapPath преобразует путь, выраженный в терминах виртуальных ката- логов (абсолютный или относительный), в путь к физическому каталогу на данном компьютере. я Метод Transfer пересылает весь текущий отклик к другому ASP-серверу для продолжения формирования отклика. Ж Метод URLEncode перекодирует URL, включая специальные символы, в строку. Объект Session Объект Session создается во время первого обращения клиента к ASP-серверу. Замечательная его особенность заключается в том, что после генерации отклика клиенту этот объект сохраняется еще некоторое время. Если в течение этого ин- тервала клиент вновь обратится к серверу, то он продолжит работу в ранее соз- данном сеансе. Если внутри этого объекта объявить переменные, то в них можно запоминать состояние клиента. Этот факт существенно облегчает проведение се- ансов с клиентом по сравнению с CGI-приложениями, где постоянно приходится
604 Глава 13. Создание ASP-объектов анализировать файлы cookie. Основные свойства, методы и события объекта Session перечислены ниже. § Свойство StaticObjects представляет собой коллекцию всех объектов, добав- ленных к сеансу при помощи тега <OBJECT>. Можно прочитать или изменить все значения свойств этих объектов. Вызов метода Remove удаляет данный объект. Вызов метода RemoveAl 1 удаляет все объекты. § Свойство Contents представляет собой коллекцию всех динамически создавае- мых объектов. Эти объекты можно создавать во время выполнения и исполь- зовать их при последующих обращениях клиентов к web-серверу. § Свойство CodePage определяет страницу кодировки символов, которая может изменяться в зависимости от локальных настроек. ж Свойство LCID определяет языковый идентификатор, используемый для ин- терпретации текстовых строк. Ш Свойство SessionlD определяет идентификатор сеанса для данного клиента. Генерируется автоматически и является постоянным для всех запросов дан- ного клиента. ж Свойство Timeout определяет время в минутах, в течение которого существует сеанс без генерации нового запроса или без вызова команды Refresh со стороны клиента. S Метод Abandon разрушает сеанс и возвращает все ресурсы. ж Событие Session_OnStart генерируется в момент создания сеанса после события Application OnStart, но до начала выполнения кода. Все переменные сеанса к этому моменту уже являются доступными и к ним можно обращаться. Обра- ботчик этого события должен быть написан на языке VBScript или JavaScript. ж Событие Sessi on_OnEnd наступает перед разрушением сеанса по истечении времени, указанном в свойстве TimeOut, или вызова метода Abandon. В обработ- чике событий можно работать с объектами Application, Server и Session, но не Request и Response. Обработчик этого события должен быть написан на языке VBScript или JavaScript. Для сохранения состояния клиента (это главная особенность) используется следующий синтаксис: Session.Уа1ие[<имя параметра;-] := «значение параметрам Рассмотрим в качестве примера Интернет-магазин. Предположим, пользователь на первой web-странице выбирает наименование товара (например, Intel Processor), на второй странице — количество этого товара (например, 5), а на третьей странице отображается цена, после чего система ожидает подтверждение заказа. Ясно, что для генерации третьей страницы необходимо знать значения параметров с первых двух страниц. Поэтому после возвращения отклика с первой страницы использу- ется команда: Session.Vaiue['GoodName'] := ' Intel Processor ': Имя выбранного товара можно получить после анализа свойств объекта Request. После второй страницы используем команду:
Работа с ASP-сервером 605 Session.Vaiue[’Total Amount'] := '5'; Перед генерацией третьей страницы можно узнать имя товара и, следовательно, цену за единицу продукции, а также количество единиц товара. Далее посредст- вом простых арифметических операций вычисляется общая стоимость и клиенту предлагается сделать заказ. В объекте Session можно запоминать и динамически создаваемые объекты, об этом будет рассказано ниже. Объект Application Объект Application используется для хранения и обработки данных, которые яв- ляются общими для всех сеансов. При помощи объекта Appl 1 cati on можно переда- вать данные от одного клиента к другому. Свойство Contents содержит список всех объектов, которые были добавлены при выполнении сценариев из ASP-документов. Объект, указанный в свойстве Contents, имеет два метода: Remove — удаляет данный объект и RemoveAll — удаляет все объекты. Сюда же можно помещать ссылки на динамически соз- даваемые объекты при выполнении кода ASP-сервера. ж Свойство StaticObjects содержит список всех объектов, которые были добав- лены при помощи тега <OBJECT>. Ж Метод Lock вызывается, когда клиенты работают в многопоточном режиме (см. главу 6), переменные в объекте Application являются общими для всех клиентов и при этом надо обратиться к свойствам объекта Application или к ме- тодам, которые работают с переменными данного объекта. После этого можно безопасно работать с переменными объекта Application, а остальные клиен- ты все это время будут ожидать окончания работы. После чтения или запи- си свойства (или окончания работы метода) требуется вызов метода Unlock, и только после этого переменные (методы) будут доступны другим клиентам. Ж Метод Unlock делает доступными переменные и методы объекта Application другим клиентам. ® Событие Appliation_OnStart наступает в момент первого старта приложения, но до создания сеанса. Из обработчика этого события можно обращаться только к объектам Server и Application. Обработчик должен быть написан на языке VBScript или JavaScript. ® Событие Application_OnEnd вызывается в момент завершения ASP-приложения, после разрушения всех сеансов. Доступные объекты и языки для описания обработчика те же, что и в обработчике Application_OnStart. Работа с ASP-сервером Типичный пример обращения к ASP-серверу из HTML-документа выглядит сле- дующим образом: <% Set Fi1 eSystem = _ Server.CreateObject("Seri pt1 ng.Fi1eSystemObject")
606 Глава 13. Создание ASP-объектов F11 eSystem.FlndAllFiles Это фрагмент кода на языке VBScript. Несмотря на наличие кода на языках сценариев (VBScript или JavaScript), ASP-страница может быть доступна клиентам, работающим в других операцион- ных системах, например UNIX. На первый взгляд это может показаться стран- ным: ведь UNIX-компьютеры не используют ни Basic, ни тем более VBScript. Но дело в том, что сценарии, содержащиеся в ASP-документах, выполняются на сер- вере, а клиент получает HTML-документ, который является результатом выпол- нения этого сценария. Серверные ASP-объекты выполняются в адресном пространстве сервера Internet Information Server (Internet Information Services), работающем под управлением операционной системы Windows NT, Windows 2000 или Windows 98. Отметим, однако, что технология ASP используется и на других платформах, в частности Solaris, AIX, HP-UX, некоторых версиях Linux, под управлением web-серве- ров других производителей, в частности Apache, благодаря семейству продуктов Chili!ASP компании ChiliSoft (www.chilisoft.net). По существу, ASP-сервер представляет собой сервер автоматизации, в кото- ром предопределено несколько интерфейсов; среди них — IRequest и IResponse. Интерфейс IRequest содержит методы, вызов которых позволяет передать пара- метры, введенные пользователем (об этом будет рассказано ниже). Интерфейс IResponse содержит методы, вызов которых приводит к формированию HTML- документа и передаче данного документа пользователю. Наличие этих при- знаков делает ASP-сервер похожим на CGI-приложения и ISAPI/NSAPI DLL (далее — web-приложения). Идеология выполнения методов в ASP-объекте и web- приложениях также аналогична: сначала анализируется запрос клиента, затем динамически формируется отклик. Различие заключается в том, что web-прило- жения формируют HTML-документ целиком, в то время как отклик ASP-объекта вставляется в исходную HTML-страницу. Например, пусть ASP-документ пред- ставлен в виде: <НТМ1_> <BODY> <TITLE> Testing Delphi ASP </TITLE> <CENTER> <H3> You should see the results of your Delphi Active Server method below </H3> </CENTER> <HR> <% Set DelphiASPObj = Server.CreateObjectt"ASP01.Test") DelphiASPObj.ScriptContent <HR> </BODY> </HTML>
Создание простейшего ASP-сервера 607 Пусть, кроме того, результат выполнения метода ScriptContent возвращает следующую строку: First call to ASP server В этом случае документ, который увидит пользователь, будет выглядеть так, как показано на рис. 13.2. Рис. 13.2. Результат выполнения запроса к простейшему ASP-серверу Иными словами, отклик ASP-объекта добавляется к HTML-документу. В од- ном документе допустимо обращение к нескольким ASP-объектам, и результат их отклика формируется в единый документ. Этого невозможно достичь при ис- пользовании ISAPI- и CGI-приложений. Создание простейшего ASP-сервера Как было сказано ранее, в Delphi 7 имеется мастер создания ASP-объектов. Для запуска этого мастера следует выбрать в главном меню среды разработки Delphi команду File ► New ► Other, перейти на страницу ActiveX репозитария объектов и активизировать значок Active Server Object. ASP-сервер, содержащий ASP-объекты, реализуется в виде исполняемых файлов (с расширением *.ехе) или динамически загружаемых библиотек (*.dll) — это разрешается при создании серверов автоматизации. ASP-сервер, реализован- ный в виде исполняемого файла, запускается единожды в ответ на запрос клиента. При использовании внутрипроцессных ASP-серверов, выполненных в виде дина- мически загружаемых библиотек, один экземпляр DLL, загруженный в адресное пространство Internet Information Server, способен обслуживать одновременно несколько клиентов. При этом для каждого клиента возможно либо создание отдельного экземпляра COM-объекта, либо обслуживание нескольких клиентов единственным экземпляром COM-объекта. Это зависит от модели потоков, вы- бранной в раскрывающемся списке Threading Model при заполнении диалогового окна, которое появляется при запуске мастера создания ASP-объектов.
608 Глава 13. Создание ASP-объектов Рассмотрим теперь, каким образом работает ASP-сервер, на конкретном при- мере создания внутрипроцессного ASP-сервера. Для простоты ограничимся сер- вером, который выполняет один запрос. Итак, выберем в главном меню среды разработки Delphi команду File ► New ► Other, перейдем на страницу ActiveX репо- зитария объектов и активизируем значок ActiveX Library. После щелчка на кноп- ке ОК в открывшемся диалоговом окне мы получим новый проект, который сохра- ним, например, под именем ASP01. Теперь снова выберем в главном меню среды разработки Delphi команду File ► New ► Other, перейдем на страницу ActiveX репо- зитария объектов и активизируем значок Active Server Object. В появившемся диалоговом окне определим имя будущего COM-класса, например Test. Поскольку мы создаем внутрипроцессный сервер, пункт, выбранный в спи- ске Instancing, не имеет значения — он важен только для исполняемых файлов. Зато в данном случае серьезную роль играет модель потоков, которая выбирается в списке Threading Model (описание моделей потоков приведено в главе 6). Группа переключателей Active Server Туре дает возможность выбрать назна- чение ASP-сервера. Если сервер планируется использовать под управлением Internet Information Server версии 3 или 4, то устанавливается переключатель Page-level event methods. Объекты, созданные с установкой этого переключателя, можно будет использовать и с Internet Information Services 5.0, но в этом случае лучше установить переключатель Object Context. Это позволит создавать объекты, работающие более эффективно. Переключатель Object Context следует устанав- ливать и в том случае, если работой ASP-сервера будет управлять Microsoft Transaction Server (Windows NT) или Component Services (Windows 2000). Фактиче- ски Internet Information Services 5.0 также управляет этим сервером при помощи Component Services — оба этих продукта тесно интегрированы. Флажок Generate a template test script for this object всегда следует оставлять установленным — Delphi в этом случае создаст небольшой HTML-документ, ко- торый можно использовать для тестирования ASP-сервера. Заполнив диалоговое окно, щелкнем на кнопке ОК, после чего будет создан файл реализации интерфейсов Unitl.pas, который следует сохранить (напри- мер, под именем ASPO1_U1). Кроме того, будет создана библиотека типов, появится ее редактор и файл, описывающий библиотеку типов, — в данном примере TEST_ TLB.pas. Если был установлен переключатель Page-level event methods (как в данном примере), то библиотека типов будет содержать два предопределенных метода — OnStartPage и OnEndPage. Также будет создан файл Test.asp, содержащий HTML- документ с заготовками на языке VBScript для тестирования сервера. Если за- глянуть в файл реализации (ASP01_U1.pas), то можно увидеть, что класс TTest является потомком класса TASPObject. Если была установлен переключатель Object Context, библиотека типов не будет содержать предопределенных методов, а сам класс TTest станет потомком класса TASPMTSObject. Оба класса-предка TTest содержат абсолютно одинаковые методы и свойства, но класс TASPObject дополнительно содержит пару методов интерфейса lASPObject — OnStartPage и OnEndPage.
Создание простейшего ASP-сервера 609 Далее создадим метод, который будет заполнять HTML-документ. Для этого в редакторе библиотеки типов выделим интерфейс ITest и щелкнем на кнопке New Method панели инструментов (рис. 13.3). Рис. 13.3. Добавление нового метода в окне редактора библиотеки типов ASP-сервера Назовем созданный здесь метод Scriptcontent, отредактировав название мето- да. Данный метод не имеет параметров. Далее щелкнем на кнопке Refresh панели инструментов окна редактора. После этого в модуле реализации (ASP01_U1.pas) появится заготовка, где следует описать реализацию. Дополним ее следующим кодом: procedure TTest.SeriptContent: begin if Assigned(Response) then Response.Write('First call to ASP server'); end: В данном примере происходит обращение к методу Write интерфейса IResponse. Проверка Assigned(Response) гарантирует, что в момент записи сообщений имеется ссылка на интерфейс. После этого следует модифицировать созданный Delphi HTML-документ для тестирования сервера, хранящегося в файле Test.asp. В этом документе имеется следующий фрагмент кода на языке VBScript: <% Set DelphiASPObj = Server.CreateObject("ASP01.Test") DelphiASPObj.{Insert Method name here} %>
610 Глава 13. Создание ASP-объектов В таком виде этот сценарий работать не будет. Необходимо заменить фразу в фигурных скобках {Insert Method name here} именем метода ASP-сервера, кото- рый генерирует отклик. В данном примере это имя ScriptContent. Исправленный фрагмент выглядит следующим образом: <% Set DelphiASPObj = Server.CreateObject!"ASP01.Test") Del phiASPObj.Scriptcontent Теперь наш проект необходимо скомпилировать, после чего можно присту- пить к тестированию созданного ASP-объекта. Для этого с помощью Internet Information Server создается виртуальный каталог, который должен иметь раз- решение как на чтение (из него будут читаться данные), так и на выполнение сценариев (из него будет загружен ASP-документ) и быть доступным по прото- колу HTTP. Поэтому, в первую очередь, необходимо обратиться к web-сервису сервера Internet Information Server, просмотреть список доступных каталогов и при необходимости создать новые с соответствующими правами доступа. В нашем примере на компьютере, который имеет IP-адрес 192.168.0.2, был соз- дан виртуальный каталог /Test, соответствующий физическому адресу C:\ASPTest. В каталог, имеющий права доступа Read и Script, был скопирован файл Test.asp. Сам файл ASP01.dll (СОМ-сервер) можно поместить в произвольный каталог. Главное — чтобы этот файл был зарегистрирован в системном реестре. Для этого после выполнения в Delphi 7 команды File ► Save As выбере.м команду Run ► Register ActiveX server. Далее в Microsoft Internet Explorer в поле Address следует ввести следующий URL-адрес: HTTP:// 192.168.0.2/Test/Test.asp Результат выполнения этого запроса был показан на рис. 13.2. Видно, что сце- нарий (текст между символами <% и %>) был замещен результатом выполнения метода ScriptContent созданного нами ASP-объекта. Теперь подробнее рассмотрим, каким образом выполняется сценарий на стра- нице Test.asp. Сервер Internet Information Server, получающий запрос о выводе этой страницы, считывает ее содержимое, находит сценарий и выполняет его. При этом запускается интерпретатор VBScript и вызывается команда CreateObject. В случае если библиотека ASP0l.dll ранее не была загружена, происходит ее за- грузка. Для данного запроса создается СОМ-объект — экземпляр класса TTest (он описан в модуле реализации, для данного примера — ASP01_U1.pas). Ссылка на интерфейс IDispatch (он поддерживается в классе TTest) сохраняется в пере- менной DelphiASPObj. При последующем написании кода после имени переменной, хранящей ссылку на IDispatch, можно набирать практически любой текст. Интерпретатор VBScript использует текст, который содержится в сценарии после имени переменной (в дан- ном примере: переменная — DelphiASPObj, метод — ScriptContent), для того чтобы передать его ASP-серверу. Если ASP-сервер найдет метод с данным именем, он его выполнит. В противном случае генерируется исключение. Поэтому при написа- нии сценариев для ASP-сервера следует быть внимательным при вводе названий
Использование HTML-форм в ASP-сервере 611 методов и при наличии исключений в первую очередь проверить корректность этих названий. При вызове какого-либо метода ASP-серверу становятся доступ- ными интерфейсы IRequest и IResponse. Использование HTML-форм в ASP-сервере Теперь рассмотрим пример создания более сложного ASP-сервера, где запрос кли- ента анализируется при помощи методов интерфейса IRequest. Задачу поставим следующим образом: дадим возможность клиенту найти запись по фрагменту поля FirstName таблицы Employees в базе данных Northwind, входящей в комплект поставки Microsoft SQL Server. Для этого в каком-либо HTML-редакторе или текстовом редакторе создадим HTML-форму, содержащую однострочное текстовое поле и кнопку Submit. HTML-документ с такой формой выглядит следующим образом: <html> <head> <meta http-equiv="Content-Type" content="text/html: charset=windows-1257"> <meta riame="GENERATOR" content="Microsoft Frontpage Express 2.0"> <title>Untitled Normal Page</title> </head> <body bgcolor="#FFFFFF"> <form action="http:// 192.168.0.2/Test/Test.asp" method="POST" name="Query"> <p>Name (fragment)<input type="text" size="20" name="Tl"></p> <p><input type="submit" name="Bl" value="Submit"></p> </form> </body> </html> При реализации этой формы вместо IP-адреса 192.168.0.2 следует указать IP- адрес компьютера, на котором установлен ASP-сервер. Поместим этот документ под именем Name.htm в каталог C:\ASPTest, к кото- рому разрешен доступ на чтение и выполнение приложений (см. выше). Но пе- ред тем как создавать модуль данных и обращаться к серверу баз данных, необ- ходимо выяснить, каким образом анализируется запрос клиента в ASP-объекте. Запрос клиента можно анализировать путем вызова методов интерфейса IRequest, ссылка на который находится в свойстве Request класса TASPObject — предка класса, где реализуется ASP-объект. Интерфейс IRequest предоставляет три свойства — QueryString, Form и Body, в которых находятся ссылки на интерфейс IRequestDictionary. Свойство QueryString содержит параметры запроса, свойство Form — список элементов управления, предоставляемых пользователю, а свойство Body — данные, которые пользователь ввел с помощью этих элементов управле- ния. Нам потребуются данные, поэтому мы будем анализировать свойство Body, однако все сказанное ниже о методах IRequestDisctionary применимо и к любому другому свойству типа ICustomDictionary — QueryString, Form.
612 Глава 13. Создание ASP-объектов Интерфейс IRequestDictionary определен в модуле ASPTIb.pas следующим об- разом: IRequestDictionary = interface!IDispatch) [’{D97A6DA0-A85F-11DF-83AE-00A0C9002BD8}'] function Get_Item(Var_: OleVariant): OleVariant: safecall; function Get__NewEnum: IUnknown; safecall: function Get_Count: SYSINT: safecall: function Get_Key(VarKey: OleVariant): OleVariant: safecall; property Item[Var_: OleVariant]: OleVariant read Get_Item; default: property _NewEnum: IUnknown read Get__NewEnum: property Count: SYSINT read Get_Count: property KeyFVarKey: OleVariant]: OleVariant read Get_Key: end: Документация о свойствах этого интерфейса в Delphi отсутствует, и остается только догадываться, каким образом из него можно извлечь параметры запроса, введенные пользователем. Привлекая документацию по компоненту TWebDi spatcher, который используется при создании CGI-приложений и ISAPI DLL, где также можно анализировать параметры запроса пользователя, можно догадаться, что свойство Count содержит число элементов управления на форме — для формы, содержащейся в документе Name.htm, оно равно 2. Свойство Key содержит имена элементов управления — для формы, содержащейся в документе Name.htm, это имена Т1 (текстовое поле) и В1 (кнопка). И наконец, свойство Item содержит вве- денные пользователем значения. Свойство Count работает, как положено, — возвращает двойку для примера, приведенного выше. Но при попытке извлечь имена элементов управления обна- руживается неприятная особенность — в коллекции Key [ ] индексы начинаются с единицы, а не с нуля, как это принято в приложениях подобного типа. Все же, обращаясь к коллекции Key с соответствующим индексом — 1 или 2, для примера, приведенного выше, можно получить названия элементов управления в виде строковых переменных. Аналогичная попытка извлечь данные, введенные пользователем с помощью элементов управления, ни к чему хорошему не приводит; так, при присвоении строковой переменной значения из коллекции I tem[] (которая объявлена анало- гично коллекции Кеу[]) происходит исключение. Анализ значения, возвращае- мого коллекцией Item[ I ], обнаруживает, что возвращается интерфейс — потомок IDispatch. Методы и свойства этого интерфейса также не описаны. Отсутствие описания интерфейса, а также возвращение ссылки на него в пере- менной типа OLEVariant, а не IDispatch обычно характерно для продуктов, которые находятся в стадии разработки. В этом случае заголовки методов интерфейса, список параметров методов и их число постоянно меняются, и для того, чтобы избежать исключений в клиентских приложениях, часто используют позднее связывание. Этот факт настораживает — не исключена возможность изменения в будущем методов интерфейса, что может привести к потере работоспособности
Использование HTML-форм в ASP-сервере 613 созданных ранее ASP-серверов. Однако хочется надеяться, что данный интер- фейс устоялся, а в Microsoft по какой-либо причине просто забыли внести изме- нения в интерфейсный модуль. Интерфейс-потомок IDispatch имеет два свойства: Count, которое возвращает всегда 1, и I tem[ ] — коллекцию, которая возвращает текст, введенный клиентом в текстовые поля. Нумерация элементов в коллекции Item начинается с индекса 1. Для тестирования запроса в ASP-объекте сделаем небольшое дополнение к проекту. Воспользовавшись редактором библиотек типов, создадим новый ме- тод RequestProp, как это было описано ранее (см. раздел «Создание простейшего ASP-сервера»). Напишем следующий код для метода RequestProp: procedure TTest.RequestProp; var S: String: V: OLeVariant; I, J, N: Integer: begin S := '1: if Assigned(Request) then if Request.Body.Count > 0 then begin for I := 1 to Request.Body.Count do begin S := S + 'Key' + IntToStrd) + '=' + Request.Body.Key[I] + '<BR>'; V := Request.Body.Itemfl]: if not VarlsEmpty(V) then if VarType(V) = varDispatch then begin N := V.Count: S := S + 'ItemCount' + IntToStrd) + '=' + IntToStr(N) + '<BR>'; if N > 0 then for J := 1 to N do S := S + V.Item[J] + '<BR>': end; end; end: if Assigned(Response) then Response.Write(S); end: Если внести данные изменения сразу же после запуска предыдущего проекта и попытаться скомпилировать проект, то компилятор остановится с ошибкой: [Fatal Error] Could not create output file ASP01.dll Причина появления этой ошибки заключается в том, что СОМ-сервер загружен и работает в памяти компьютера. При загрузке исполняемого файла в память Win- dows не дает возможности переписать его на диске. Поэтому перед компиляцией СОМ-сервер необходимо выгрузить. Для выгрузки DLL используются разные
614 Глава 13. Создание ASP-объектов методы, которые зависят от версии Internet Informaton Server (Internet Informaton Services). К Internet Informaton Services 5.0 Запустить Internet Services Manager, выделить в левой части окна этого при- ложения имя компьютера, используемого в качестве web-сервера, в списке доступ- ных для него служб найти пункт Default Web Site и выделить виртуальный ката- лог Test. Щелкнуть на нем правой кнопкой мыши и в контекстном меню выбрать команду Properties. В открывшемся диалоговом окне перейти на вкладку Virtual Directory, щелкнуть на кнопке Unload. Я Internet Informaton Seiner 4.0 При работе под Windows NT необходимо остановить службы WWW и WEB site Administration. Затем надо запустить эти службы в обратном порядке. При работе под Windows 98 требуется перезагрузка компьютера. К Internet Informaton Server 3.0 Достаточно остановить и повторно запустить службу WWW. После выгрузки библиотеки ASP01.dll скомпилируем проект и в созданном ранее файле Test.asp изменим код на языке VBScript следующим образом: вме- сто строки Del phi ASPObj.Scri ptContent напишем строку Del phi ASPObj.RequestProp. После этого в Internet Explorer необходимо обратиться к странице Name.htm сле- дующей командой: http://192.168.0.2/Test/Name.htm Здесь вместо символов 192.168.0.2 следует набрать IP-адрес сервера. В полу- ченной форме введем какое-либо значение в текстовое поле и щелкнем на кнопке Submit (рис. 13.4). Name (fragment)|AN | Submit | Рис. 13.4. Ввод клиентом данных для отправки их на web-сервер В итоге мы получим результат выполнения приведенного выше кода метода RequestProp: Кеу1=Т1 ItemCountl=l AM Кеу2=В1 ItemCount2=l Subtint Итак, для определения параметров, введенных клиентом в какой-либо эле- мент управления, необходимо просмотреть всю коллекцию Keys, найти индекс интересующего нас элемента управления (в данном примере это 1, что соответ-
Доступ к базам данных в ASP-сервере 615 ствует ключу Т1) и извлечь значение, введенное клиентом, посредством вызова команды: Request.Body.Itemflndex].Itemfl] Теперь можно перейти к модификации имеющегося сервера — созданию но- вого метода для доступа к базам данных. Доступ к базам данных в ASP-сервере Модуль данных, в который можно помещать невизуальные компоненты, масте- ром создания ASP-объекта не генерируется — его необходимо создавать от- дельно. Поэтому выберем команду File ► New ► Data Module. В результате к проекту будет добавлен модуль данных, который мы сохраним под именем ASP01_U2.pas. В этот модуль данных будут помещены иевизуальные компоненты доступа к дан- ным (визуальные компоненты в ASP-объектах использовать нельзя). Вообще говоря, в web-приложениях (ASP, ISAPI/NSAPI, CGI) попытки ото- бражения модальных форм с элементами управления (а диалоговые окна — част- ный случай таких форм) ни к чему хорошему не приводят. При выводе диалого- вого окна на нем появятся элементы управления, и приложение будет ожидать его закрытия (щелчком на кнопке ОК или Cancel), чтобы продолжить свою работу. Особенностью же web-приложений является то, что такое диалоговое окно не- видимо. Поэтому его нельзя закрыть ни щелчком на кнопке (они не получают сообщения WM_LBUTTONDOWN операционной системы), ни нажатием клавиш быстрого вызова (сигналы с клавиатуры не посылаются невидимым элементам управле- ния). Визуально программист наблюдает следующее: приложение «висит», от- клика от ASP-объекта клиент не получает, для повторной компиляции проекта требуется перезапуск Internet Information Server или перезагрузка операционной системы. Даже если команды отображения диалоговых окон в ASP-сервере в явном виде отсутствуют, они все равно могут появиться в процессе работы приложения — например, какая-либо из библиотек, используемых приложением, пришлет сообще- ние об ошибке. Данный факт надо принимать во внимание при написании кода, в котором необходимо тщательно проверять данные перед их использованием, чтобы внешние приложения или библиотеки не присылали сообщений об ошибках в виде диалоговых окон. В течение многих лет доступ к данным в Delphi осуществлялся с помощью механизма Borland Database Engine (BDE), при этом необходимо было использо- вать компоненты TSession, TDatabase и TQuery. Однако при создании ASP-объектов для доступа к данным выяснилось, что при применении BDE они непригодны для доступа к серверам баз данных — при попытке соединиться с базой данных после передачи параметров, содержащих имя пользователя и пароль, происходит исключение (данный факт был проверен для Oracle 8i и InterBase 5.5). Через BDE удалось получить доступ только к базе данных DBDEMOS, которая не требует аутентификации пользователя при обращении к данным. К сказанному следует добавить, что в операционных системах Windows 2000 и Windows NT с установлен- ным пакетом обновления Service Pack 5 или 6 нельзя получить доступ к данным
616 Глава 13. Создание ASP-объектов через механизм BDE и в традиционных ISAPI- и CGI-приложениях, в отличие от Windows 95/98 и Windows NT с пакетом Service Pack 4.0 или ниже. К счастью, в последних версиях Delphi имеются альтернативные способы дос- тупа к данным — с помощью технологий ADO (ActiveX Data Objects), начиная с версии Delphi 5, и dbExpress, начиная с версии Delphi 6. Для работы с ADO, прежде всего, необходим компонент TADOConnection. Поместим его в модуль дан- ных. В инспекторе объектов выберем свойство Connectionstring и вызовем диалого- вое окно создания строки. В списке на вкладке Provider выделим пункт Microsoft OLE DB Provider for SQL Server и щелкнем на кнопке Next (рис. 13.5, слева). Рис. 13.5. Установка параметров соединения с сервером Microsoft SQL Server 7.0 На вкладке Connection диалогового окна необходимо указать имя сервера (в дан- ном примере — TREPA) и параметры аутентификации — имя пользователя (SA) и па- роль (рис. 13.5, справа). Поскольку пароль не нужен, нужно установить флажок Blank password. Обязательно следует установить флажок Allow saving password, иначе ASP-объект попытается вывести на экран диалоговое окно ввода имени пользователя и пароля. Протестировать соединение можно щелчком на кнопке Test Connection — должно появиться сообщение об успешном соединении с сервером. Далее в инспекторе объектов необходимо установить свойство LoginPromp рав- ным False. Проверить правильность установок можно при помощи свойства Connected, попытавшись установить его равным Тгие. При этом не должны появ- ляться ни диалоговое окно ввода имени пользователя и пароля, ни информация об исключении. Свойство DefaultDatabase компонента ADOConnectionl установим равным Northwind (имя демонстрационной базы данных, поставляемой вместе с Microsoft SQL Server).
Доступ к базам данных в ASP-сервере 617 Поместим компонент TADOQuery в модуль данных и в его свойстве Connection сошлемся на компонент ADOConnectionl. Следует учесть, что только что разработанный модуль данных автоматически при загрузке ASP-сервера или при обращении клиента к ASP-объекту создаваться не будет. Поэтому необходимо переписать конструктор и деструктор класса TTest, реализация которого находится в файле ASP01_U1 .pas. Сошлемся на модуль ASP01_ U2.pas в модуле ASP01_U1.pas. В объявлении класса TTest в секции private опре- делим переменную FData типа TDataModulel. В секции public объявим процедуры AfterConstruction и BeforeDesctruction с обязательной директивой override: TTest = class(TASPObject. ITest) private FData: TDataModulel; protected public procedure AfterConstruction: override: procedure BeforeDestruction: override; end: Реализуем процедуры AfterConstruction и BeforeDestruction в секции реализации: procedure TTest.AfterConstructi on; begin inherited: FData := TDataModulel.CreateCnil): end: procedure TTest.BeforeDestructi on: begin if Assigned(FData) then begin FData.ADOQueryl.Active := False; FData.ADOConnectionl.Connected := False: FData.Free: end: inherited; end: Далее в окне библиотеки типов (см. рис. 13.3) следует создать новый метод, который назовем QueryResponse. Реализуем его следующим образом: procedure ТТest.QueryResponse: var S: String; I. J: Integer; begin S := Request.Body.Item[l].Item[l]; if FData.ADOQueryl.Active then FData.ADOQueryl.Close: FData.ADOQueryl.SQL.Cl ear;
618 Глава 13. Создание ASP-объектов FData.ADOQueryl.SQL.Add('select * from dbo.employees'): FData.ADOQueryl.SQL.AddCwhere FirstName like + S + T"): FData.ADOQueryl.Active := True: if FData.ADOQueryl.RecordCount > 0 then begin FData.ADOQueryl.First; for J := 0 to FData.ADOQueryl.Fields.Count - 1 do Response.Wr1te(FData.ADOQueryl.Fields[J],FieldName + ' '): Response.Write('<BR>'): for I := 1 to FData.ADOQueryl.RecordCount do begin for J := 0 to FData.ADOQueryl.Fields.Count - 1 do Response.Write(FData.ADOQueryl.Fields[JJ.AsString + ' '); Response.Wri te('<BR>'); if FData.ADOQueryl.RecordCount then FData.ADOQueryl.Next: end: end: end: В данном методе динамически создается SQL-запрос, при этом используются параметры, введенные клиентом в форму, представленную на рис. 13.4. С этим запросом происходит обращение к серверу баз данных, и возвращаемые данные помещаются в HTML-документ. В созданном ранее файле Test.asp вместо строки DelphiASPObj.ScriptContent напишем строку DelphiASPObj.QueryResponse. После этого запустим Microsoft Internet Explorer и снова обратимся к странице Name.htm. Результат выполнения запроса приведен на рис. 13.6. "3; ; Links -’-г Back » ,'.Я -71 ^Search Favorites ^History i -j Address http://192.168.0.2/Test/Test.asp History X ' Viejjt » ^Search I ZlToday You should see the results of your Delplii Active Server method below Employee!!) LastName FirstName Title TitleOfCourtesy BirthDate HireDate Address City Region PostalCode Country HomePhone Extension Photo Notes ReportsTo PhotoPath 1 Davolio Nancy Sales Representative Ms. 12/8/1948 5/1/1992 507 - 20th Ave. E Apt. 2A Seattle WA 98122 USA (206) 555-9857 5467 □ П/Education includes a BA in psychology from Colorado State University in 1970. She also completed “The Art of the Cold Call.*' Nancy is a member of Рис. 13.6. Результат обращения ASP-объекта к серверу баз данных
Дополнительные возможности ASP-сервера 619 Дополнительные возможности ASP-сервера При использовании ASP-объекта необходимые для его работы параметры можно поместить в HTML-документ. Эти параметры могут редактироваться в документе, что позволяет менять параметры конкретного сайта. Это удобно при поставке ASP-сервера: купившая его компания может изменить начальные установки так, чтобы они соответствовали требованиям компании. Для этого достаточно отре- дактировать HTML-документ даже силами специалистов низкой квалификации. Пример проиллюстрируем следующим образом: определим в заголовке класса TTest (ASP01_U1.pas) две переменные: FCompanyName:String и FCopyrightYear:String. Определим в окне библиотеки типов (см. рис. 13.3) два новых свойства: CompanyName: String и CopyrightYear: Integer. В методах Read и Write для этих свойств определим чтение и возвращение данных из описанных выше переменных. В библиотеку типов добавим новый метод ShowCopyright, который реализуем следующим образом: procedure TTest.ShowCopyright: var S: OLEVariant; begin S := Format!'Copyright (С) M by fcs'. [FCopyrightYear. FCompanyName]): if Assigned(Response) then Response.Write(S): end; В созданном ранее файле Test.asp изменим код на языке VBScript: <% Set DelphiASPObj = Server.CreateObjectC'ASPOl.Test") DelphiASPObj.CompanyName = "My Company" DelphiASPObj.CopyrightYear = 2001 DelphiASPObj.ShowCopyright %> Обратимся к ASP-объекту при помощи команды (вместо символов 192.168.0.2 следует ввести IP-адрес вашего сервера): http://192.168.0.2/Test/Test.asp Результатом будет генерация страницы, показанной на рис. 13.7. Если в файле Test.asp изменить имя компании (а это можно сделать при по- мощи любого текстового редактора), соответствующие изменения отобразятся в HTML-документе. При помощи ASP-объектов можно передавать не только текстовую информа- цию, но и двоичную, например графические изображения. Создадим обычную HTML-страницу следующего вида: <HTML><BODY> <TITLE> Picture display </TITLE> <CENTER><H3> Refer to picture from ASP</H3></CENTER> <HR><IMG SRC="http://192.168.0.2/Test/Testl.asp" border="0"><HR> </BODY></HTML>
620 Глава 13. Создание ASP-объектов Рис. 13.7. Отображение ресурсов в HTML-документе Сохраним файл в каталоге C:\ASPTest — он экспонируется как виртуальный каталог /Test. Файл Testi .asp выглядит следующим образом: <% Set DelphiASPObj = Server.CreateObject("ASP01.Test") DelphiASPObj.GetPicture %> Этот файл находится в том же каталоге. При создании файла следует обра- тить внимание на отсутствие в нем тегов <HTML>, <BODY> и т. д., а также любых записей, которые могли бы быть интерпретированы как текст в формате HTML. В нем должно быть только обращение к методу ASP-объекта и больше ничего. В противном случае появится сообщение о недопустимости изменения типа от- клика после записи заголовков в HTML-документ. Метод GetPicture реализуем следующим образом: procedure TTest.GetPicture: var BM: TBitmap; OV: OLEVariant: S: TMemoryStream; P: Pointer; begin Response.Buffer := True: Response.Cl ear; Response.ContentType := 'image/bmp'; BM := nil; S ;= nil; try BM := TBitmap.Create; BM.LoadFromFile('C:\finance'): S := TMemoryStream.Create: BM.SaveToStream(S): OV := VarArrayCreate([l. S.Size], varByte); P := VarArrayLock(OV); Move(S.MemoryЛ. P", S.Size);
Хранение информации о состоянии 621 VarArrayllnlock(OV); Response.BinaryWrite(OV): Response.End: finally BM.Free: S.Free: end: end: Для передачи двоичных данных необходимо указать их тип в свойстве Response. ContentType и воспользоваться методом BinaryWrite объекта Response. Параметром этого метода является переменная типа OLEVa riant. В этой переменной должны находиться двоичные данные в виде массива байтов. Этот массив передается как отклик. В корневой каталог диска (С:\) поместим картинку — web-приложения (в том числе ASP-приложения) могут открывать файлы в любых каталогах, а не только в тех, которые доступны внешним пользователям, обратившимся к Internet Information Server через Интернет. В результате обращения к исходному HTML- документу клиент получит отклик (рис. 13.8). Рис. 13.8. Результат получения двоичных данных от ASP-объекта Хранение информации о состоянии Автоматическая обработка файлов cookie в технологии ASP позволяет опреде- лить, обращался ли ранее клиент к данному web-серверу. При первом обращении клиента для него создается объект Session. При последующих обращениях этого же
622 Г лава 13. Создание ASP-объектов клиента для него назначается ранее созданный объект Session. Объект Session разрушается либо если в течение интервала времени Session.TimeOut не было новых запросов от клиентов, либо при вызове метода Session.Abandon из кода. В объекте Session можно хранить переменные и объекты, которые динамически создаются или изменяются. Эти изменения видны при каждом последующем обращении клиента. Таким образом, объект Session может хранить информацию о состоянии клиента. Возможность хранения информации о состоянии, реализованная на уровне техно- логии, предоставляет ASP-приложениям существенные преимущества по сравне- нию с CGI- и ISAPI-приложениям, в которых для хранения информации о состоя- нии требуется писать сложный код, предназначенный для работы с файлами cookie. Сначала разберемся, каким образом можно сохранять и изменять перемен- ные. Для этого в секции private класса TTest модуля ASP01JJ1 объявим метод: TTest = cl ass(TASPObject, ITest) private procedure IncrementCallCount(const Name: String): Реализуем этот метод в секции implementaton: procedure TTest.IncrementCallCcunt(const Name: String); var S: String: N. I: Integer: begin S := Session.Value[Name]: if Length(S) = 0 then S : = 'O': Val(S. N. I): Inc(N); Session.Vaiue[Name] := N; end; В каждом методе модуля ASP01_lll вызовем метод IncrementCallCount с пара- метром Иате=<имя метода>. Например, в ранее объявленном методе GetPicture об- ратимся к методу IncrementCallCount следующим образом: IncrementCallCount!'GetPicture'): Далее добавим в библиотеку типов новый метод GetCall Count, как это было описано ранее. Реализуем этот метод следующим образом: procedure TTest.GetCallCount: var S. SI: String: begin IncrementCallCount!'GetCal1 Count'): SI •= Session.Value['RequestProp']: S := 'RequestProp ' + SI + '<BR>': SI := Session.Value['TestSession']; S := S + 'TestSession ' + SI + '<BR>': SI := Session.Value['GetPicture']:
Хранение информации о состоянии 623 S := S + 'GetPicture ' + SI + ’<BR>'; SI := Session.Vaiue['ShowCopyright']; S := S + 'ShowCopyright ' + SI + '<BR>'; SI := Session.Valuef'QueryResponse']; S := S + 'QueryResponse ' + SI + '<BR>'; SI := Session.Vaiue['RequestProp']: S := S + 'RequestProp ' + SI + '<BR>'; SI := Session.Value[’GetCallCount']: S := S + 'GetCallCount ' + SI + '<BR>'; SI := Session.Value['SeriptContent']: S := S + 'Scriptcontent ' + SI; Response.Write(S): end; И, наконец, создадим ASP-документ с вызовом метода GetCallCount: <% Set DelphiASPObj = Server.CreateObject("ASP01.Test") De1 ph i AS PObj.GetCa11 Count %> Сохраним этот документ в файле Test3.asp и в каталоге C:\ASPTest (напом- ним, что он зарегистрирован в Internet Information Server и имеет разрешения на чтение и выполнение кода). Далее, обращаясь к ранее созданным ASP-доку- ментам, начнем вызывать объявленные методы ASP-сервера в произвольной по- следовательности и произвольное число раз и, наконец, обратимся к странице Test3.asp. В результате получим примерно такой отклик, как на рис. 13.9. Рис. 13.9. Результат выполнения метода GetCallCount в ASP-сервере
624 Глава 13. Создание ASP-объектов Напротив названия каждого метода стоит число обращений к нему клиента в текущем сеансе. Если в браузере щелкнуть на кнопке Refresh, то можно заме- тить, что число напротив надписи GetCa 11 Count увеличится на 1. Данные в коллекции Session.Value хранятся в виде вариантных переменных — иными словами, они могут принадлежать любому типу, приводимому к типу OleVariant: Integer, single, double, WideString и т. д. Можно хранить и массивы — для этого вызывается функция VarArrayCreate. Доступ к переменным осуществля- ется по имени. Коллекция Session.Value возвращает пустое значение (null), если ранее переменная с данным именем не была добавлена к коллекции. Добавление новой переменной осуществляется при присвоении значения элемента коллек- ции с уникальным именем. Однако ссылки на экземпляры классов (объекты) нельзя хранить в этой кол- лекции. Точнее говоря, их хранить можно — например, привести указатель к типу Integer и сохранить в коллекции Session.Value. Далее можно получать доступ к объекту путем приведения типа — и, таким образом, при каждом следующем обращении клиента получать ссылку на объект и работать с ним. Казалось бы, все хорошо — однако при разрушении сеанса по истечении времени, указанном в свойстве, TimeOut деструкторы объектов не будут вызваны. Это означает, что при работе такого сервера постепенно будут исчерпываться доступные систем- ные ресурсы, и в конце концов web-сервер просто «рухнет». Если можно было бы сделать обработчик события Session_OnEnd в коде Delphi, то деструкторы допускалось бы вызвать явно. Однако этот обработчик можно сделать только в ASP-документе на языке VBScript или JavaScript. Выход из создавшейся ситуации заключается в использовании коллекции Session.Contents, которая предназначена для хранения объектов. В принципе, для хранения объектов также предназначена коллекция Session.StaticObjects, но ее нельзя модифицировать во время выполнения приложения. Идея хранения объ- ектов в коллекции Session.Contents заключается в следующем: объекты, кото- рые необходимо хранить, поддерживают интерфейс IUnknown (или его потомка). Именно ссылка на этот интерфейс и помещается в коллекцию Session.Contents. При закрытии сеанса для всех интерфейсов, хранящихся в этой коллекции, вы- зывается метод Release. Соответственно, из этого обработчика событий можно вызвать деструктор класса при равенстве нулю числа ссылок на интерфейс. Для проверки сказанного выше поставим задачу следующим образом: мы хо- тим, чтобы каждую запись в базе данных можно было вывести на отдельную web- страницу и чтобы пользователь мог передвигаться по записям вперед и назад. При такой постановке задачи требуется запоминать положение указателя теку- щей записи в наборе данных — и это будет делаться в компоненте Session. Наи- более простой способ запомнить положение указателя текущей записи — не за- крывать набор данных после формирования отклика пользователю. Первоначально создадим ASP-документ, содержащий форму с двумя кноп- ками — Back и Next В этом документе вызовем новый метод ASP-сервера, который назовем, например, TestSession. В этом методе будем анализировать запрос и сме- щать текущую запись вперед или назад. Сохраним документ в файле Test4.asp. Окончательный вид документа следующий:
Хранение информации о состоянии 625 <HTML> <BODY> <TITLE> Testing Delphi ASP </TITLE> <CENTER> <H3> Database navigation </H3> </CENTER> <form action="http://192.168.0.2/Test/Test4.asp" method="POST" name="Navigate"> <p> <1nput type="submit" name="Bl" value="Prior"> <1nput type="submit" name="Bl" value=”Next"> </p> </form> <HR> Set DelphlASPObj = Server.CreateObject("ASP01.Test") DelphlASPObj.TestSession %> <HR> </BODY> </HTML> Далее, создадим новый модуль данных и поместим на него компоненты TADOConnection и TADOTable. В компоненте ADOConnectionl сошлемся на базу данных Northwind. В компоненте ADOTablel сошлемся на компонент ADOConnectionl, свой- ству TableName присвоим значение Alphabetical list of products, а его свойство Active установим равным True. Это означает, что немедленно после отработки конст- руктора будет установлена связь с сервером баз данных. Объявим новый интер- фейс ISessionObject — потомок IUnknown. Полезно к этому интерфейсу добавить метод, который будет возвращать указатель на модуль данных. Хотя из-за необ- ходимости маршалинга между разными модулями применять указатели в интер- фейсах запрещено, в данном случае это оправдано, поскольку этот метод будет использоваться только внутри модуля ASP01. Все внешние приложения (в том числе Internet Information Server) будут работать с этим интерфейсом как с IUnknown и не будут «знать» о существовании метода, возвращающего указатель. Как объяв- лять новые интерфейсы и реализовывать их, рассказывалось в главе 1. Окончательный код выглядит следующим образом: unit ASP01_U3; interface uses SysUtils. Classes. DB. ADODB: type ISessionObject = interface!IUnknown) [•{3F5F10ED-5556-4D9B-B05B-7A0014D0D629}'] function getDataModule:pointer: stdcall: end:
626 Глава 13. Создание ASP-объектов T0ataModule2 = class(TDataModule. ISessionObject) ADOConnectionl: TADOConnection; ADOTablel: TADOTable; private FRefCount: Integer; function getDataModule: Pointer; stdcall; function ISessionObject.Queryinterface = ObjQuerylnterface; function ISessionObject._AddRef = ObjAddRef; function ISessionObject._Release = ObjRelease: public function ObjAddRef: Integer; virtual: stdcall: function ObjQueryInterface(const IID: TGUID; out Obj): HResult; virtual: stdcall: function ObjRelease: Integer; virtual; stdcall; end; implementation {$R *.dfm} function TDataModule2.getDataModule: Pointer; begi n Result := Self; end; function TDataModule2.0bjQueryInterface(const IID: TGUID; out Obj): HResult; begin if GetInterfacedID. Obj) then Result := S_OK else Result ;= E-NOINTERFACE: end; function TDataModule2.ObjAddRef; Integer; begi n Inc(FRefCount); Result := FRefCount: end: function TDataModule2.ObjRelease: Integer; begi n Dec(FRefCount); Result := FRefCount: if Result = 0 then Destroy: end: end.
Хранение информации о состоянии 627 Данный код простой и не требует комментариев. Ключевой метод — ObjRelease — уменьшает число ссылок и при равенстве их нулю вызывает деструктор класса. Некоторые изменения требуются и в классе TTest модуля ASP01JJ1. Прежде всего, необходимо вызвать конструктор класса TDataModule2. Наиболее подходящее место для вызова конструктора — событие Session_OnStart — недоступно из кода Delphi. Метод AfterConstruction также мало подходит — при обращении к нему не определены ссылки на объекты Session, Application, Server. Необходимо, чтобы эти объекты были инициализированы из интерфейса IScriptingContext — и это делает метод OnStartPage. Поэтому будем вызывать конструктор из метода TTest. OnStartPage. При этом следует учитывать, что конструктор мог быть вызван раньше — во время предыдущего запроса клиента. Следовательно, необходима предварительная проверка существования экземпляра класса TTest. Окончатель- но код выглядит следующим образом: procedure TTest.OnStartPage(const AScriptingContext: IUnknown): var V: OleVariant: FD: TDataModule2: lu: ISessionObject; begin inherited OnStartPage!AScriptingContext): V := Session.Contents.Item['DataModule']: if VarlsEmpty(V) then begin FD := TDataModule2.Create(nil): FD.Get Interface!ISessionObject. lu): V := lu: Session.Contents.ItemfDataModule'] := V: end: end: Первоначально в переменную V пытаемся копировать переменную с име- нем DataModule, которая хранится в объекте Session. Если это не удается (а это происходит, когда пользователь обращается к ASP-серверу в первый раз), то создаем экземпляр класса TDataModule2, получаем от него ссылку на интерфейс ISessionObject и запоминаем ее в объекте Session под именем DataModule. Теперь поведение экземпляра класса TDataModule2 окажется корректным — он будет существовать, пока существует объект Session для данного клиента, и разрушаться с окончанием сеанса. Чтобы в этом убедиться, достаточно переписать деструктор TDataModule2 и поместить туда команду Веер (визуальные элементы управления в ASP-приложениях показывать запрещено). Если после этого вновь обратиться к проекту ASP01 и затем выгрузить ASP-сервер, как это было описано выше, то можно услышать звук динамика. Осталось реализовать метод TestSession в классе TTest — он формирует отклик для клиента. В этом методе необходимо получить доступ к хранящемуся в объекте Session экземпляру класса TDataModule2, проанализировать запрос и при щелчке
628 Глава 13. Создание ASP-объектов на кнопке Next переместить курсор на следующую запись, а при щелчке на кнопке Prior — на предыдущую. Окончательно код выглядит следующим образом: procedure TTest.TestSession: var V: OleVariant: I: Integer: FD: TDataModule2; S: String: lu: IUnknown: begin S := ": if Request.Body.Count > 0 then if Request.Body.Item[l],Count > 0 then S := Request.Body.Item[l].Item[l]; IncrementCallCount('TestSession'); V := Session.Contents.Itemt’DataModule']: lu := IUnknown!V); FD := (lu as ISessionobject).getDataModule; if Assigned(FD) then begin if length(S) > 0 then begin if AnsiCompareText(S, 'PRIOR') = 0 then FD.ADOTablel.Prior else FD.ADOTablel.Next; end: S := "; for I := 0 to FD.ADOTablel.FieldCount - 1 do S := S + FD.ADOTablel.Fields[I].AsString + '<BR>'; Response.Write(S); end; end: В этом коде используется определенный ранее метод ISessionObject.getDataModule для получения ссылки на экземпляр класса TDataModule2. Скомпилировав этот проект и обратившись к созданной ранее странице Test4.asp, можно получить от- клик (рис. 13.10). При щелчке на кнопке Next получаем страницу со следующей записью; при щелчке на кнопке Prior — с предыдущей. В заключение следует отметить, что объект Application также имеет анало- гичные свойства для хранения переменных и объектов. Эти свойства имеют те же самые названия, что и в объекте Session. Существует два отличия в использо- вании одноименных свойств объекта Application. Переменные и объекты, хранящиеся в объекте Application, доступны всем кли- ентам, работающим с данным ASP-сервером. Если бы описанный выше про- ект хранился в объекте Appl i cati on, к серверу могли бы обратиться два клиента. В этом случае если бы первый клиент щелкнул на кнопке Next, то при щелчке вторым клиентом на кнопке Next он увидел бы, что указатель текущей записи смещается не на одну, а на две записи вперед.
Создание внепроцессных ASP-серверов 629 Рис. 13.10. Навигация по набору данных в ASP-приложении И Разные клиенты обращаются к объекту Appl icati on из разных потоков выпол- нения. Поэтому необходимо вызывать метод Appl1 cati on. Lock перед началом обращения к данным, хранящимся в Appl i cati on. Игнорирование этого требова- ния приведет к периодическим и невоспроизводимым ошибкам. После окон- чания работы с данными необходимо вызывать метод Application.Unlock — иначе остальные клиенты не смогут получить доступ к данным, хранящимся в объекте Application. Создание внепроцессных ASP-серверов До сих пор мы рассматривали внутрипроцессные серверы, которые работают в ад- ресном пространстве Internet Infomation Server и реализуются в динамически загружаемых библиотеках (DLL). В заключение этой главы следует также по- говорить о создании внепроцессных ASP-серверов. Такие серверы реализуются в виде исполняемых файлов и работают в отдельном адресном пространстве. Описанная далее процедура создания такого сервера тестировалась в Internet Information Server 4.0 под управлением операционной системы Windows NT 4.0 с установленным пакетом Service Pack 5. Для создания внепроцессного сервера необходимо открыть готовый проект, компиляция которого приводит к созданию исполняемого файла, или создать
630 Глава 13. Создание ASP-объектов новый проект, выбрав команду File ► New Application. После этого следует вы- брать команду File ► New ► Other и на странице ActiveX окна репозитария объектов активизировать значок Active Server Object. В результате будет сгенерирована библиотека типов, содержащая методы OnStartPage и OnEndPage. Все, что было ска- зано выше по поводу внутрипроцессного сервера, справедливо и по отношению к внепроцессному серверу, разработка ASP-объекта заключается в создании но- вых методов, которые будут вызываться из VBScript-кода ASP-страницы. Сложности возникают при попытке протестировать внепроцессный сервер. По умолчанию параметры Internet Information Server настроены так, что запуск этим сервером приложений запрещен — разрешена только загрузка DLL. Более того, в администраторе Internet Information Server отсутствует параметр, позво- ляющий разрешить или запретить использование приложений как ASP-серве- ров. Для разрешения на запуск исполняемого файла как ASP-сервера необходимо выполнить следующий код: Set oWebService = GetObject("IIS://LocalHost/W3svc") oWebService.Put "AspAllowOutOfProcComponents". True oWebServ1ce.SetInfо Пользователь, инициирующий выполнение данного сценария, должен иметь статус администратора. По этой причине данный сценарий бесполезно опреде- лять в HTML-документе и запускать его, используя Internet Explorer: любой пользователь Интернета имеет статус гостя (Guest), а не администратора. Сце- нарий необходимо поместить в обработчик какого-либо события в среде разра- ботки Visual Basic или VBA и запустить его оттуда (либо использовать Windows Scripting Host). К сожалению, в Delphi нет метода, аналогичного методу GetObject языка VBScript. Очевидно, что метод GetObject возвращает ссылку на интерфейс IDispatch Internet Information Server. Однако при этом в качестве параметра он использует строку, которая не относится к классу (ее GUID отсутствует в сис- темном реестре). В Delphi аналогичных методов нет, по крайней мере, в виде простых вспомогательных функций. Возможно, подобный метод станет доступ- ным в следующих версиях Delphi. Заключение Данная глава посвящена созданию ASP-объектов, используемых для генерации данных, внедряемых web-сервером в HTML-документы. В ней мы изучили ASP- объекты, такие как Response, Server, Session, Application, а также исследовали во- просы создания простейших ASP-приложений. Далее мы рассмотрели пример создания простейшего ASP-сервера, а также пример применения в ASP-серверах HTML-форм. Мы обсудили реализацию доступа к базам данных в ASP-серверах, приме- нимость различных универсальных механизмов доступа к данным в подобных приложениях, а также дополнительные возможности, которые можно реализо- вать в ASP-серверах, такие как хранение информации о состоянии данных под-
Заключение 631 ключенных клиентов. В заключение мы обсудили некоторые аспекты создания внепроцессных ASP-серверов. Мы убедились в том, что технология ASP предлагает мощные инструменты для создания web-приложений. Возможность добавлять результат выполнения кода ASP-сервера к HTML-документу, вызов нескольких ASP-серверов из одного HTML-документа и сохранение информации о состоянии данных клиента в объ- екте Session выгодно отличает эту технологию от традиционных технологий CGI и ISAPI. Отметим, что для компаний, широко использующих технологии Micro- soft, эта технология, равно как и создание собственных ASP-серверов, является предпочтительным способом публикации данных в Интернете. Отметим, однако, что возможности создания распределенных приложений средствами СОМ отнюдь не исчерпываются технологией DataSnap и разра- боткой COM-серверов для применения их в ASP-страницах. Еще одна из техно- логий создания распределенных приложений, основанных на технологии СОМ, заключается в использовании служб компонентов Microsoft. Именно вопросы создания таких приложений мы и обсудим в следующей главе.
ГЛАВА 14 Службы компонентов Данная глава посвящена созданию и применению COM-серверов, используемых совместно со службами Microsoft Component Services, называемых иногда служ- бами компонентов (само же расширение СОМ, позволяющее создавать такие приложения, получило название СОМ+). Применение подобных серверов позво- ляет решать задачи, не предусмотренные спецификацией СОМ, такие как авто- ризованный доступ к COM-серверам и организация распределенных транзакций, а также в ряде случаев снизить требования к ресурсам, необходимым для экс- плуатации СОМ-серверов. Назначение служб компонентов Разработчики и пользователи СОМ-серверов нередко сталкиваются с различными проблемами, возникающими на этапе их эксплуатации при одновременном обслу- живании COM-серверами большого числа клиентов. В частности, при создании СОМ-серверов доступа к данным, обслуживающих нескольких клиентов, следу- ет позаботиться о поддержке нескольких соединений с базой данных и о работе с несколькими потоками выполнения. Как мы уже знаем (см. главу 12), создание подобного кода с помощью технологии DataSnap не представляет особых слож- ностей. Однако при большом числе обслуживаемых клиентов такие приложения предъявляют серьезные требования к потребляемым им ресурсам, например, из-за необходимости поддерживать множество соединений с базой данных. Созда- ние же и уничтожение СОМ-объектов по требованию клиентских приложений, особенно объектов, предоставляющих доступ к данным, требует определенных затрат времени, что неблагоприятно сказывается на производительности таких приложений. Поэтому нередко разработчики пытаются создать дополнительный код для осуществления совместного доступа многих клиентов к нескольким экземплярам СОМ-объектов, постоянно находящихся в оперативной памяти (иногда говорят о так называемом пуле объектов), при этом число последних долж- но быть по возможности минимальным. При необходимости обращения к СОМ- объекту клиентское приложение может воспользоваться одним из объектов, по- заимствовав его из пула, а затем вернуть его назад, не инициируя ни его создания, ни его уничтожения.
Назначение служб компонентов 633 ПРИМЕЧАНИЕ ------------------------------------------------------------ Создание пула DataSnap-объектов возможно в случае применения компонента TWebConnection в клиентском приложении и библиотеки HTTPSRVR.DLL в качестве универсального клиента сервера доступа к данным. Пример реализации пула по- добных объектов можно найти в примерах из комплекта поставки Delphi (каталог Delphi7\Demos\Midas\Pooler). Еще одна проблема, обычно возникающая в процессе практической реализа- ции проектов, содержащих COM-серверы, заключается в том, что реально созда- ваемые приложения, в отличие от учебных и книжных примеров, могут иметь довольно сложную архитектуру и, в частности, содержать несколько различных сервисов промежуточного слоя для решения тех или иных задач. Например, некоторые из них могут отвечать за предоставление доступа к данным из несколь- ких разнотипных СУБД (особенно это актуально в компаниях, когда-то под- вергшихся так называемой «островковой» автоматизации), и при этом может возникнуть потребность в инструменте, реализующем комплексную функцио- нальность, например осуществление распределенных транзакций, затрагивающих базы данных, обслуживаемые разными серверными СУБД (например, Microsoft SQL Server и Oracle). Имеется также ряд проблем, связанных с авторизованным доступом пользо- вателей к сервисам, предоставляемым COM-серверами. Эти вопросы, если рас- сматривать их в рамках традиционной технологии СОМ, целиком и полностью «ложатся на плечи» разработчиков этих сервисов. Спецификация СОМ не со- держит никаких требований на этот счет. Таким образом, имеется потребность в расширении технологии СОМ за счет сервиса, обеспечивающего создание СОМ-объектов для совместного использо- вания многими клиентами, авторизованный доступ к этим объектам, а также при необходимости обработку транзакций этими объектами. Расширенная таким об- разом технология СОМ получила название СОМ+, а сам сервис, реализующий это расширение и являющийся составной частью Windows 2000, получил назва- ние служб компонентов Microsoft (Microsoft Component Services). ПРИМЕЧАНИЕ ---------------------------------------------------------- Предыдущая версия этого сервиса входит в состав Windows NT 4 Option Pack (сво- бодно распространяемого пакета обновления для Windows NT 4.0) и носит название Microsoft Transaction Server 2.0 (MTS). Службы компонентов обеспечивают централизацию применения СОМ-серве- ров, совместное использование многими клиентами пула СОМ-объектов и ресур- сов, в частности соединений с базой данных, а также управление транзакциями. Отметим, что помимо создания СОМ-объектов для коллективного пользования, а также предоставления сервисов авторизации для доступа пользователя к объек- там и обработки транзакций службы компонентов предоставляют средства мони- торинга объектов и транзакций.
634 Глава 14. Службы компонентов Прежде чем приступить к созданию приложений с применением служб ком- понентов, мы обсудим основные принципы их работы. Принципы работы служб компонентов Рассмотрим типичный сценарий работы приложения, использующего службы компонентов. Пользователь запускает клиентское приложение, реализованное в виде испол- няемого файла или web-приложения. Клиентское приложение пытается устано- вить соединение с объектом СОМ+. Если компонент СОМ+, содержащий такой объект, реализован в виде внутрипроцессного сервера, он может выполняться в адресном пространстве приложения dllhost.exe или в адресном пространстве клиентского приложения. Если речь идет о Windows NT и Microsoft Transaction Server, то компонент, содержащий такой объект, должен быть реализован во внутрипроцессном сервере и выполняться в адресном пространстве приложения mtx.exe (объект MTS может выполняться еще и в адресном пространстве клиент- ского приложения). В общем случае в объекте СОМ+ может быть реализована практически любая функциональность, например, такой объект вполне может быть сервером доступа к данным. Если запрос на установку соединения корректен, в адресном пространстве того приложения, в котором должен функционировать объект, создается так назы- ваемый контекст (context) объекта (если при этом приложение dllhost.exe не за- пущено, происходит его запуск). Контекст объекта содержит дополнительные сведения, которые не предусмотрены спецификацией СОМ и потому не со- держатся в COM-объектах. Это такие сведения, как правила доступа к объекту и способ участия его в транзакциях. Контекст создается для каждого клиента, обслуживает именно его (в отличие от собственно COM-объекта) и «по поруче- нию» клиента взаимодействует с COM-объектом, который может быть только что создан или взят из имеющегося пула объектов. После создания контекста клиентское приложение может посылать объекту СОМ+ запросы на вызов его методов. Эти методы могут, например, выполнять запросы к СУБД или реализовывать какие-то иные действия, в том числе и обра- щаться к другому объекту СОМ+ (в последнем случае мы можем говорить о «ро- дительских» и «дочерних» объектах). Если при вызове какого-либо метода объекта СОМ+ (родительского или до- чернего) возникает исключение, объект информирует об этом службы компонен- тов, а они, в свою очередь, информируют об этом обратившееся к этому объекту приложение (или, соответственно, другой объект СОМ+), а также при необходи- мости производят дополнительную обработку исключения. Если же метод объекта СОМ+ успешно завершил свою работу (неважно, с по- мощью дочерних объектов или без таковой), он информирует об этом службы компонентов, равно как и о том, что клиент в нем больше не нуждается. После этого данный объект может быть разрушен, возвращен в пул или использован другим клиентом.
Принципы работы служб компонентов 635 После этого краткого введения рассмотрим подробнее особенности функцио- нирования объектов СОМ+. Организация пулов объектов и ресурсов В общем случае словосочетания «организация пула ресурсов» и «организация пула объектов» означают, что в приложении создается некоторое количество ре- сурсов определенного типа (например, соединений с базой данных). Если клиенту данного приложения потребуется такой ресурс (или, соответственно, объект), он не создается, а берется из пула и по окончании работы возвращается в пул, а не уничтожается. Зачем нужна организация пулов объектов? Нередко бывает, что серверные объекты при их создании потребляют немалый объем иных ресурсов, например, создавая большие временные файлы, увеличивая сетевой трафик и т. д. Исходя из изложенных выше соображений, такие типы объектов эффективнее создать однократно в заранее определенном количестве с целью последующего их ис- пользования клиентскими приложениями. Если в какой-то момент число клиен- тов, которым нужны такие серверные объекты, превысит количество имеющихся их экземпляров, должны быть созданы дополнительные экземпляры. Уничтоже- ние дополнительных экземпляров ставших ненужными объектов должно произ- водиться в соответствии с установленным для них заранее максимальным време- нем существования в неактивном состоянии. Реализация пулов ресурсов в СОМ+ осуществляется с помощью специальных объектов, которые кэшируют ресурсы для того, чтобы организовать их коллектив- ное использование несколькими объектами СОМ+, объединенными в приложе- ние. Эти объекты носят названия распределителей ресурсов (resource dispensers) и менеджеров ресурсов (resource managers). Распределители ресурсов предназна- чены для кэширования общих данных, не подверженных изменению с помощью транзакций, а менеджеры ресурсов — для управления транзакциями, то есть для реализации стандартных требований к транзакциям (таких как их изоляция и атомарность). При создании приложений СОМ+ довольно часто применяется организация пула соединений с базами данных, в частности пула соединений с ODBC- и OLE DB-источниками. При применении такого пула снижается сетевой трафик между службами компонентов и сервером баз данных за счет снижения частоты установки и разрыва соединений с сервером. Организация таких пулов поддерживается драй- верами, удовлетворяющими спецификациям ODBC 3.0 и ODBC 3.5, и некоторыми OLE DB-провайдерами, в частности OLE DB Provider for Microsoft SQL Server. Управление транзакциями Транзакция в широком смысле — это группа операций, которые либо все вместе выполняются, либо все вместе отменяются. Чаще всего говорят о транзакциях в базе данных и операциях над данными, в ней содержащимися. Однако в общем случае транзакция может затрагивать несколько баз данных, возможно, разных производителей, либо включать действия, не связанные с операциями, произво- димыми над данными.
636 Глава 14. Службы компонентов Для управления распределенными транзакциями используется специальный сервис — Microsoft Distributed Transaction Coordinator (MS DTC), который может быть активизирован либо из окна просмотра служб компонентов (прило- жения Component Services Explorer, входящего в средства администрирования объектов СОМ+ и доступного в разделе Administrative Tools панели управления Windows 2000), либо из приложения Service Control Manager панели управления Windows 2000, либо из приложения Enterprise Manager из комплекта поставки Microsoft SQL Server 7.0 или 2000 (если таковой имеется на данном компьютере). MS DTC представляет собой службу операционной системы, координирующую транзакции, использующие различные менеджеры ресурсов, и позволяющую реализовать действия, в рамках единой транзакции затрагивающие различные базы данных, возможно, управляемые серверами разных производителей и нахо- дящиеся на разных компьютерах. Как в СОМ+ реализованы распределенные транзакции? Когда какой-либо объект СОМ+ запрашивает доступ к базе данных, драйвер соответствующей СУБД проверяет в контексте этого объекта, требуется ли ему транзакция, и, если требуется, информирует об этом DTC. Затем драйвер обращается к менеджеру ресурсов, отвечающему за базу данных, с запросом о начале транзакции в ней. Только после этого компонент может начать манипулировать данными с помо- щью этого драйвера. Если нужно изменить данные в двух базах данных с помощью единой тран- закции, DTC и первый из драйверов инициируют транзакцию в первой базе дан- ных так, как это было описано выше. При открытии соединения со второй базой данных DTC и второй драйвер обращаются к соответствующему менеджеру ре- сурсов с запросом о начале новой транзакции для осуществления изменений, производимых во второй базе данных. В результате в обеих базах данных откры- ваются транзакции. Когда объект СОМ+, ответственный за какую-либо часть такой транзакции, завершает свою работу, он вызывает собственный метод SetComplete, если вы- полняемые им операции завершились успешно, либо метод SetAbort, если про- изошел откат транзакции. Далее DTC обращается к обоим менеджерам ресурсов с запросом на сохранение результатов или откат обеих транзакций. Таким обра- зом, изменения в обеих базах данных будут либо вместе сохранены, либо вместе отменены (рис. 14.1). Если объект СОМ+ должен предотвратить сохранение результата или откат распределенной транзакции, он может вызвать собственный метод DisableCommit. Метод EnableComnrit позволяет DTC сохранить результаты всех открытых несо- храненных транзакций. ПРИМЕЧАНИЕ ---------------------------------------------------------- Как правило, подобного рода транзакции возможны с участием СУБД, поддерживаю- щих двухфазное завершение транзакций (two-phase commit). Сведения о том, поддер- живает ли выбранная СУБД двухфазное завершение транзакций, должны содержаться в документации, поставляемой с этой СУБД.
Принципы работы служб компонентов 637 Клиентское приложение Рис. 14.1. Реализация распределенной транзакции с помощью DTC Следует отметить, что части распределенной транзакции обычно реализуют в различных объектах СОМ+. Ранее мы уже отмечали, что один объекты, на- зываемые родительскими, могут инициировать создание других объектов, назы- ваемых дочерними. Если такой дочерний объект генерирует исключение в процессе манипуляции с базой данных, службы компонентов будут проинформированы о необходимости отката транзакции. В этом случае DTC инициирует откат тран- закции родительского объекта и уведомляет об этом клиентское приложение. Поэтому для реализации подобного управления транзакциями следует помес- тить код, реализующий различные части распределенной транзакции, в разные объекты СОМ+, а затем создать для них родительский объект. При использова- нии такого механизма контроля транзакций клиентское приложение может не содержать ни кода, связанного с сохранением результатов или откатом транзак- ций, ни кода, отвечающего за соединение с базой данных. Вопросы безопасности Службы компонентов позволяют задействовать список пользователей и групп пользователей Windows 2000/ХР в качестве списка пользователей своих объектов.
638 Глава 14. Службы компонентов При этом для каждого объекта можно установить правила его эксплуатации раз- личными пользователями и группами пользователей. Помимо этого, службы компонентов поддерживают механизм ролей. Роль в службах компонентов — это совокупность пользователей и групп пользователей, которые имеют право обра- щаться к интерфейсам объектов, включенных в данное приложение СОМ+. При использовании ролей в объекты СОМ+ можно не включать код реализации пра- вил безопасности. Отметим, что роли служб компонентов не следует интерпретировать как ана- логи ролей серверных СУБД — последние представляют собой не совокупность пользователей, а совокупность привилегий. Рассмотрев возможности служб компонентов, мы должны обсудить сами объ- екты СОМ+, а именно, каким требованиям они должны удовлетворять и как они регистрируются. Особенности объектов СОМ+ Требования к объектам СОМ+ Объекты СОМ+ являются серверами автоматизации и, как все подобные серверы, реализуют интерфейс IDispatch. Все объекты СОМ+ поддерживают специфический для них интерфейс lObjectControl, содержащий методы для активации и деактивации объекта СОМ+ и управления ресурсами (в том числе соединениями с базами данных). Серверный объект СОМ+ должен иметь стандартную фабрику классов и библио- теку типов (они автоматически создаются при использовании мастера Transactional Object Wizard). Можно редактировать библиотеку типов, добавляя свойства и ме- тоды (в дальнейшем они будут использованы службами СОМ+ для получения сведений об объектах СОМ+). Помимо этого, компонент должен, как и все внутри- процессные серверы автоматизации, экспортировать функцию DI 1 Reg 1 sterServer и осуществлять саморегистрацию своего идентификатора CLSID (мастер Tran- sactional Object Wizard генерирует соответствующий код автоматически). Если предполагается коллективное использование серверного объекта, он не должен хранить внутри себя сведения о состоянии данных, связанных с конкрет- ным клиентом, например результатов запросов, номера текущей записи в наборе данных, и т. д., а немедленно пересылать такие сведения клиентскому приложе- нию. Объекты, удовлетворяющие этому требованию, называются не хранящими информации о состоянии (stateless objects). Заметим, что таковыми являются DataSnap-серверы, созданные в Delphi 6 и 7, а также MIDAS-серверы, созданные в Delphi 5. MIDAS-серверы, созданные в Delphi 4 и Delphi 3, таковыми в общем случае не являются, и для того чтобы они удовлетворяли подобным требованиям, следует реализовать в них ряд дополнительных методов. В общем случае при создании кода объектов СОМ+ рекомендуется пользо- ваться соединением с базой данных минимальное время и как можно быстрее вернуть его в соответствующий пул ресурсов. Ссылка же на контекст объекта при этом может сохраняться достаточно долго. По этой же причине рекоменду-
Особенности объектов СОМ+ 639 ется также вызывать метод SetCompl ete как можно чаще, чтобы как можно быст- рее вернуть объект в соответствующий пул объектов. Особенности управления объектами СОМ+ Объекты СОМ+ объединяются в приложения СОМ+ (СОМ+ Applications), кото- рые в MTS назывались пакетами (packages). Каждое из приложений СОМ+ может содержать один или несколько объектов. Управление объектами и приложе- ниями СОМ+ осуществляется с помощью приложения Componet Services Explorer, которое доступно в разделе Administrative Tools панели управления Windows 2000. Каждый объект, зарегистрированный как объект СОМ+, обладает свойством Transaction Support, определяющим правила участия объекта в транзакциях. Это свойство принимает пять возможных значений: М Disabled — для данного объекта не создается контекст транзакции; М Not Supported — службы компонентов не запускают компонент внутри контек- ста транзакции; Ж Supported — службы компонентов запускают компонент внутри контекста транзакции, если таковая запрошена, и без него в противном случае; S Requi red — службы компонентов помещают объект в контекст транзакции при вызове любого из методов; если для этого объекта транзакция недоступна, инициируется новая; S Requi res new — службы компонентов инициируют новую транзакцию при созда- нии объекта независимо от того, какие транзакции в этот момент выполняются. Создавая объект СОМ+ с помощью мастеров Delphi Transactional Object Wizard и Transactional DataModule Wizard, мы можем выбрать нужное значение этого свойства. Обсудив особенности управления объектами СОМ+, рассмотрим классы Delphi, отвечающие за их создание. Классы Delphi для создания объектов В Delphi 7 Enterprise имеются два класса для создания приложений СОМ+ — TMtsAutoObject и TMtsDataModule. Оба класса предназначены для создания серверов автоматизации, реализующих интерфейс lObjectControl (строго говоря, реализа- ция этого интерфейса обязательна только для объектов MTS, но не обязательна для объектов СОМ+, но и в последнем случае она рекомендуется). В отличие от класса TMtsAutoObject, класс TMtsDataModule представляет собой модуль данных, в который можно помещать невизуальные компоненты. Он реализует интерфейс lAppServer, позволяющий применять в приложениях СОМ+ DataSnap-компоненты. Оба эти класса реализуют ряд методов, характерных для объектов СОМ+. М SetCompl ete — вызывается после того, как объект СОМ+ успешно выполнил свою работу и далее не нуждается в данных, связанных с обслуживаемым им клиентом. После вызова этого метода объект становится неактивным. Если объект выполняется в контексте транзакции, его вызов означает, что эта часть транзакции готова к завершению.
640 Глава 14. Службы компонентов » SetAbort — вызывается в случае, если возникнет необходимость отката тран- закции, в которой участвует данный объект. Если транзакция была запущена автоматически службами компонентов специально для этого объекта, она бу- дет отменена, а объект уничтожен. Если же объект был частью «чужой» тран- закции, ее результаты не будут сохранены. Обычно этот метод вызывается при обработке исключений в блоке except предложения try...except. И EnableCommlt — вызывается, если объект СОМ+ может сохранить результаты своей транзакции, не избавляясь от данных, связанных с клиентом. Он не инициирует уничтожения объекта. Его вызов означает, что транзакцию с уча- стием этого объекта можно завершить. S DisableCommit — вызывается, если объект не может сохранить результаты те- кущей транзакции до тех пор, пока не будет вызван метод EnableCommlt или метод SetComplete. S IsInTransaction — с помощью этого метода можно определить, выполняется ли данный объект внутри транзакции. Ж IsCallerlnRole — этот метод обладает входным параметром типа WideString. С его помощью можно проверить, соответствует ли клиент роли, имя которой указано во входном параметре. И IsSecurityEnabled — этот метод позволяет проверить, защищен ли объект службами безопасности СОМ+. Создание серверных объектов Возможности, предоставляемые службами компонентов, намного шире, чем те, которые мы проиллюстрируем примерами — если подробно рассматривать их все, пришлось бы писать отдельную книгу. Однако, на наш взгляд, нельзя обойти вниманием такие вопросы, как создание объектов для доступа к данным, по- скольку на сегодняшний день это наиболее распространенный тип задач, и реали- зация распределенных транзакций — подобная задача сейчас также нередко встречается на предприятиях, подвергшихся в свое время «островковой» авто- матизации и имеющих на сегодняшний день разнородные приложения, нуждаю- щиеся в интеграции и использующие СУБД разных производителей. Именно эти задачи мы и рассмотрим в оставшейся части нашей главы. Изучение вопросов создания объектов СОМ+ мы начнем с создания про- стейшего сервера доступа к данным, который позволяет манипулировать дан- ными таблицы Products, уже знакомой нам по предыдущим главам базы данных Northwind, входящей в комплект поставки Microsoft SQL Server, и клиентского приложения для его тестирования. Затем мы создадим второй объект СОМ+ для манипуляции данными в таблице, хранящей сведения о заказанных товарах (она будет находиться в той же базе данных). Затем мы создадим объект, выполняю- щий транзакцию, затрагивающую обе таблицы, и активизирующий оба ранее созданных объекта внутри своей транзакции. И наконец, мы перенесем таблицу Products из базы данных Microsoft SQL Server 2000 в базу данных Oracle 8 и вне-
Создание серверных объектов 641 сем соответствующие изменения в компоненты доступа к данным нашего первого объекта СОМ+, чтобы рассмотреть, как распределенные транзакции управляются с помощью сервиса Distributed Transaction Coordinator. Клиентские приложения и объекты СОМ+ могут располагаться как на одном и том же компьютере, так и на разных компьютерах одного домена (в этом слу- чае мы можем обеспечивать доступ с помощью DCOM). Компьютер, на котором расположены объекты СОМ+ (далее — серверный компьютер), должен иметь доступ к базам данных, к которым они будут обращаться. Для доступа к данным обеих СУБД мы будем использовать универсальный механизм доступа к данным ADO, а результаты запросов в клиентское приложе- ние будем передавать в виде наборов данных ADO (ADO Recordset). Если представ- ленные здесь примеры будут выполняться под управлением Windows NT (и, со- ответственно, использоваться не службы компонентов, a MTS), следует удосто- вериться, что на клиентском и серверном компьютерах установлены библиотеки MDAC (Microsoft Data Access Components) и служба DCOM, а на серверном компьютере — сервер MTS (напомним, что он входит в состав пакета Windows NT Option Pack, а последний доступен на web-сервере корпорации Microsoft). Перед тестированием компонентов следует также запустить сервис Distributed Transaction Coordinator. В нашем первом примере мы расскажем, как создать простейший объект СОМ+, а затем создадим несложное клиентское приложение для его тестирования. Создание объекта СОМ+ для доступа к данным Первый объект СОМ+, который мы создадим, будет манипулировать данными в таблице Products базы данных Northwind из комплекта поставки Microsoft SQL Server 7.0 или 2000. В нем мы реализуем методы для уменьшения значения поля UnitsInStock и увеличения значения поля UnitsOnOrder данной таблицы (эти опе- рации производятся, когда со склада заказывают товар), а также для отправки ре- зультатов в клиентское приложение с целью контроля результатов. Структуру нашего первого приложения иллюстрирует рис. 14.2. Создание объекта СОМ+ мы начнем с нового проекта библиотеки ActiveX, выбрав команду File ► New ► Other и активизировав значок ActriveX Library на стра- нице ActiveX окна репозитария объектов. Сохраним проект под именем STOCK. Да- лее на странице Multitier окна репозитария объектов выберем значок Transactional Data Module и заполним диалоговое окно соответствующего мастера. Как и в слу- чае обычного СОМ-объекта, нам следует ввести имя COM-класса (назовем его Stock_Data) и выбрать модель потоков (обычно в объектах СОМ+ применяется модель разделенных потоков — пункт Apartment в списке Threading Model). По- мимо этого следует выбрать значение свойства Тransacti on Support создаваемого объекта. Для этого в списке Transaction Model выберем пункт Requires a transaction — это позволит в дальнейшем использовать данный объект в качестве участника транзакции, инициированной другим объектом. После этого будет сгенериро- ван модуль данных (потомок класса TMtsDataModule) и соответствующая биб- лиотека типов.
642 Глава 14. Службы компонентов Клиентское приложение Приложен» для тестщхиктия p,oductlCijPrc..JuctName < , J Chai 2 Chang 3 Aniseed Syrup 4 Chef Anton's Cajun Sea omng 6 Grandma's Boysenberry Spread 7 Uncle Bob's Oraamc Dned Pears 8 Northwoods Cranberry Sauce {unitPnce furntsInStockj UnftsOnOtdeil * j 18 0: 39 5. 71 0 о 6 > 0 Число зл- 334Н1ЫК единиц товара 5 5 5? 120 15 Б Службы компонентов 19 10 22 25 30 40 DecJJnitsInStock L -► IncJJnitsOnOrder Stock_Data Рис. 14.2. Простейшее приложение COM+ Теперь поместим в созданный модуль данных компоненты TADOConnection и TADOCommand. Свойство Connection компонента ADOCommandl установим равным ADOConnectionl, а свойство Connectionstring компонента ADOConnectionl заполним следующим образом: Provi der=SQLOLEDB.1;Password="”; Persist Security Info=True:User ID=sa; Initial Catalog=Northwind;Data Source=MAINDESK Отметим, что имя сервера, имя пользователя и пароль на вашем компьютере могут быть другими. Следует также установить свойство LoginPrompt компонента ADOConnectionl равным False с целью избежать появления диалогового окна аутен- тификации пользователя на серверном компьютере при попытке активации созда- ваемого объекта. Теперь откроем библиотеку типов созданного объекта и создадим в ней три новых метода. Первый из них, DecJJnitsInStock, будет уменьшать значение поля UnitsInStock одной из записей таблицы Products, второй, IncJJnitsOnOrder, будет увеличивать значение поля UnitsOnOrder той же записи. Оба эти метода должны иметь два целых входных параметра — ProductID, равный значению первичного ключа, идентифицирующего запись, и OrdQuantity, равный количеству единиц заказанного товара. Третий метод (назовем его Get ProductList) будет возвращать в клиентское приложение набор данных ADO с результатом запроса к таблице Products. Чтобы иметь возможность использовать такой тип данных, сошлемся
Создание серверных объектов 643 на библиотеку Microsoft ActiveX DataObjects Recordset 2.6 Library в библиотеке типов объекта stock (это можно сделать, выбрав команду Show All Type Libraries в контекстном меню страницы Uses окна редактора библиотеки типов и выде- лив указанную выше библиотеку в появившемся списке). Для создания метода Get_ProductL1st определим свойство ProductList типа Recordset, доступное только для чтения. Результат редактирования библиотеки типов представлен на рис. 14.3. Рис. 14.3. Библиотека типов объекта Stock Data Описав методы объекта СОМ+, щелкнем на кнопке Refresh панели инстру- ментов редактора библиотек типов. Теперь нам следует реализовать эти методы. Первый из них, как было сказано ранее, уменьшает значение поля UnitsInStock. Возможные значения этого поля должны быть больше или равны нулю (таково ограничение, присутствующее в базе данных и отражающее очевидное бизнес-пра- вило — число единиц товара на складе не должно быть отрицательным). Следо- вательно, этот метод является потенциальным источником исключения, связан- ного с нарушением серверного ограничения. Поэтому мы учтем это факт в коде и обработаем исключение согласно правилам, принятым в СОМ+ и обсуждав- шимся ранее в этой главе. Таким образом, реализация данного метода имеет вид: procedure TStock_Data.DecJJnitsInStock(ProductID. OrdQuantity: Integer): begin try 11 Соединяемся с базой данных ADOConnectlonl.Open: 11 Текст запроса к базе данных ADOCommandl.CommandText := 'UPDATE Products SET UnitsInStock=UnitsInStock-' +IntToStr(OrdQuantity)+' WHERE ProductID=' +IntToStr(ProductID);
644 Глава 14. Службы компонентов // Выполняем запрос ADOCommandl.Execute; // Разрываем соединение с базой данных ADOConnectionl.Close: // Информируем службы компонентов об успешном И выполнении запроса SetComplete; except ADOConnecti onl.Close: // Информируем службы компонентов о неудачной попытке И выполнения запроса SetAbort: // Передаем исключение вызывающему приложению raise: end; end: Второй метод изменяет значение поля UnitsOnOrder. Здесь мы также обработаем возможное исключение — ведь исключения могут возникать не только из-за нару- шения серверных ограничений, но и по иным причинам (например, из-за разрыва соединения с базой данных). Реализация этого метода имеет вид: procedure TStock_Data.Inc_UnitsOnOrder(ProductID. OrdQuantity: Integer); begin try // Соединяемся с базой данных ADOConnecti onl.Open; // Текст запроса к базе данных ADOCommandl.CommandText := 'UPDATE Products SET UnitsOnOrder=UnitsOnOrder+' +IntToStr(OrdQuantity)+' WHERE ProductID=' +IntToStr(ProductID); // Выполняем запрос ADOCommandl.Execute: 11 Разрываем соединение с базой данных ADOConnecti onl.Close: // Информируем службы компонентов об успешном И выполнении запроса SetComplete: except 11 Разрываем соединение с базой данных ADOConnecti onl.Cl ose: // Информируем службы компонентов о неудачной попытке И выполнения запроса SetAbort: // Передаем исключение вызывающему приложению raise: end: end:
Создание серверных объектов 645 Третий метод обращается с запросом к таблице Products для получения ее со- держимого в виде набора данных ADO. Вот его реализация: function TStock_Data.Get_ProductList: Recordset: var QRY: String: begin QRY := 'SELECT ProductID. ProductName. UnitPrice. ' + 'UnitsInStock. UnitsOnOrder FROM Products ' + 'WHERE Discontinued=0': try // Создаем набор данных Result := CoRecordset.Create: Result.CursorLocation := adUseClient: // Открываем его Result.Open(QRY.ADOConnecti onl.Connect!onStri ng. adOpenStatic.adLockBatchOptimiStic. adCmdText): // Разрываем соединение с базой данных ADOCOnnecti onl.Close: // Информируем службы компонентов об успешном И выполнении запроса SetComplete: except // Разрываем соединение с базой данных ADOConnecti onl.Close: // Информируем службы компонентов о неудачной попытке И выполнения запроса SetAbort: // Передаем исключение вызывающему приложению raise: end: end; Для работоспособности данного метода следует сослаться в секции uses на модули ADODB, ADODB_TLB и ADOR_TLB (последний генерируется при ссылке на биб- лиотеку типов Microsoft ActiveX DataObjects Recordset 2.6 Library). Теперь нам следует скомпилировать и сохранить проект, а затем зарегистри- ровать его. Если разработка компонента, содержащего наш объект, велась на сер- верном компьютере, следует выбрать в главном меню Delphi команду Run ► Install СОМ+ Objects (в этом случае следует ввести имя нового приложения СОМ+, в ко- торый должен устанавливаться объект, — пусть оно называется СОМР1 us_Demo). Если же разработка объекта велась не на серверном компьютере, созданный компонент — файл STOCK.DLL — следует скопировать в любой каталог серверного компьютера и зарегистрировать его с помощью приложения для управления службами компонентов (Component Services Explorer). В этом случае создадим новое приложение СОМ+, щелкнув правой кнопкой мыши на элементе СОМ+ Applications раздела Computer в левой части окна просмотра служб компонентов и выбрав в контекстном меню команду New ► Application. Назовем наше приложение COMPlus_Demo. Далее в новое приложение, пока ничего не содержащее, добавим
646 Глава 14. Службы компонентов созданный компонент. Для этого раскроем список папок созданного приложе- ния, в контекстном меню раздела Components выберем команду New ► Component и заполним появившиеся диалоговые окна, ответив на вопросы, касающиеся устанавливаемого компонента. Можно также просто перетащить имя файла компонента СОМ+ в окно просмотра служб компонентов. После этого можно найти и приложение, и сам компонент в приложении для управления службами компонентов (рис 14.4). Рис. 14.4. Компонент Stock_Data, зарегистрированный в службах компонентов Создав простейший компонент СОМ+ и зарегистрировав его в службах ком- понентов, мы можем приступить к созданию клиента. Тестирование объекта СОМ+ для доступа к данным Клиент служб компонентов будет представлять собой обычное Windows-приложе- ние (это может быть и web-приложение, но в данном случае мы ограничимся Win- dows-приложением). Создадим новый проект, поместим на его форму компоненты TRDSConnection и TADODataSet. Свойство ServerName компонента RDSConnectionl устано- вим равным программному идентификатору (ProgID) созданного ранее объекта
Создание серверных объектов 647 СОМ + (в данном случае stock.Stock_Data), а свойство ComputerName должно быть равным имени серверного компьютера. Как уже говорилось ранее, для соедине- ния с удаленным компонентом в этом случае используется технология DCOM. Никаких свойств компонента ADODataSetl в процессе разработки устанавли- вать не нужно — он просто получит набор данных на этапе выполнения. Однако нам следует продемонстрировать полученный набор данных пользователю, по- этому поместим на форму компоненты TDataSource, TDBGrid и TDBNavigator и со- шлемся в них на компонент ADODataSetl. Наконец, поместим компонент TEdit для ввода числа единиц заказанного товара и две кнопки, с помощью которых мы будем вызывать методы объекта СОМ+. Следующим шагом является создание обработчиков события OnClick этих кнопок. Первый из них будет получать от объекта СОМ+ набор данных и ото- бражать их: procedure TForml.ButtonlClickCSender: TObject): begin try // Создаем контекст обьекта Stock_Data RDSConnectionl.Connected : = True: // Получаем набор данных у объекта Stock_Data RS := RDSConnectionl.GetRecordsetC'ProductList'. "): ADODataSetl.Recordset := RS: // Показываем его пользователю ADODataSetl.Open: // Теперь можно вызывать другие методы этого объекта Button2.Enabled : = True; except // Объект не может вернуть данные ShowMessage(' Список товаров недоступен'); // Тогда мы не можем вызывать его методы Button2.Enabled := False; end: // Освобождаем контекст обьекта RDSConnectionl.Connected ;= False; end; Обработчик события OnCl i ck другой кнопки эмулирует обработку заказа, вы- зывая два других метода нашего объекта: procedure TForml.Button2Click(Sender: TObject): var PID. Quantity ; Integer: begin // Идентифицируем товар по значению поля ProductID PID := ADODataSetl.F1eldByName('ProductID').Aslnteger; // Определяем, сколько единиц товара мы хотим заказать Quantity := StrToInt(Editl.Text): try
648 Глава 14. Службы компонентов // Создаем контекст объекта StockJJata RDSConnectlonl.Connected := True: И Вычитаем значение Quantity из значения И поля UnitsOnOrder RDSConnectlonl.AppServer.DecJJnitsInStock(PID. Quantity): // Добавляем Quantity к значению поля UnitsOnOrder RDSConnectlonl.AppServer.IncJJnitsOnOrder(PID. Quantity); ButtonlClick(Self); except // В объекте возникло исключение ShowMessageC'Заказ не принят'); end; // Освобождаем контекст объекта RDSConnectlonl.Connected := False; end; Нам следует также описать переменную RS: var Forml : TForml; RS ; -Recordset; Теперь мы можем скомпилировать и сохранить клиентское приложение, а за- тем скопировать его на клиентский компьютер. Запустим приложение и щелкнем на первой кнопке. Мы должны получить набор данных, который будет отображен в компоненте TDBGrid (рис. 14.5). Если при этом будет открыто и окно управления службами компонентов, можно наблю- дать анимацию значка компонента, к которому происходит обращение. Анимация должна завершиться, как только закончится пересылка данных, — она свидетель- ствует о том, что объект находится в активном состоянии. Рис. 14.5. Клиентское приложение, использующее объект Stock Data Теперь можно выбрать одну из записей полученного набора данных, вве- сти целое число (например, 1) в компонент Editl и щелкнуть на второй кнопке.
Управление транзакциями 649 После этого набор данных должен обновиться, значение поля Units InStock в вы- бранной записи — уменьшиться, а значение поля UnitsOnOrder — увеличиться на введенную в компонент Editl величину. Однако, если мы попытаемся ввести в компонент Editl число, превышающее значение UnitsInStock, объект СОМ+, выполняя запрошенный метод с этим параметром, инициирует нарушение огра- ничения на значение этого поля в базе данных и, соответственно, возникновение исключения в самом объекте СОМ+. После этого все манипуляции с данными будут прекращены, а исключение передано вызывающему приложению, которым в данном случае является наше клиентское приложение. В нем-то мы и увидим сообщение об ошибке: «Заказ не принят». Обратите внимание на то, что сведения о количестве выполненных и отменен- ных транзакций можно найти в подразделе Transaction Statistics раздела Distributed Transaction Coordinator окна просмотра служб компонентов (рис. 14.6). Рис. 14.6. Сведения о количестве выполненных и отмененных транзакций Итак, мы научились создавать простейшие объекты СОМ+, регистрировать и тес- тировать их. Нашим следующим шагом будет реализация в разных объектах тран- закции, состоящей из нескольких частей. Этому будет посвящен следующий раздел. Управление транзакциями Чтобы реализовать комплексную (и в общем случае распределенную) транзак- цию, обычно требуется создание нескольких объектов СОМ+, а также организа- ция обращений их друг к другу.
650 Глава 14. Службы компонентов Реализация транзакций Создадим пример приложения СОМ+, реализующий комплексную транзакцию, для чего добавим еще два объекта к уже имеющемуся. Первый из объектов будет обладать единственным методом, добавляющим запись в таблицу, в которой ре- гистрируются заказы (назовем ее OrderedProducts, и, прежде чем создавать сам объект, мы создадим и эту таблицу). По своей реализации новый объект похож на объект Stock_Data, созданный нами ранее. Во втором из объектов мы реализуем комплексную транзакцию, которая потребует следующих манипуляций. 1. Добавление новой записи, содержащей наименование и количество единиц заказанного товара, а также сведения о заказчике, полученные из клиентского приложения, в таблицу OrderedProducts. 2. Уменьшение значения поля Units InStock выбранной записи таблицы Products на целое число, переданное из вызывающего приложения. 3. Увеличение значения поля UnitsOnOrder той же самой записи таблицы Products на ту же величину. Взаимодействие частей этого приложения схематически изображено на рис. 14.7. Для реализации подобной транзакции один из объектов должен обращаться к двум дочерним объектам. Один из них, выполняющий второе и третье действие из приведенного списка, stock.Stock_Data, нами уже создан. Приступим к созда- нию объекта, выполняющего первое действие. Но перед этим, как мы и обещали, создадим в базе данных Northwind таблицу OrderedProducts, выполнив следую- щее SQL-предложение: CREATE TABLE OrderedProducts ( Ord_ID int IDENTITY (1. 1) NOT NULL PRIMARY KEY Address char (40) NULL. Orderedltem char (50) NULL, UnitPrice money NULL. Quantity ) int NULL Обратите внимание на то, что в этой таблице мы создали поле Ord_Id типа Identity (в Microsoft SQL Server так называются поля, обычно используемые в первичных ключах таблиц, значение которым при создании новой записи при- сваивается автоматически). Чуть позже с его помощью мы продемонстрируем, что именно происходит в базе данных при откате транзакции. Значения полей Address и Quantity будут получены из клиентского приложения, а значения полей Ordered_Item и UnitPrice — из таблицы Products. Создав таблицу, займемся созданием объекта СОМ+. Используя уже описан- ную ранее последовательность действий, создадим библиотеку ORDERS.DLL, ко- торая будет содержать объект СОМ+ с именем Orders_Data и значением свойства Transaction Support равным Requires a transaction (для этого выберем одноимен- ный пункт в списке Transaction model).
Управление транзакциями 651 В модуль данных, который при этом будет создан, поместим точно такой же набор компонентов, что и в предыдущем объекте, с точно такими же значе- ниями свойств (для этой цели можно создать соответствующий шаблон компо- нентов). Далее откроем библиотеку типов созданного объекта и добавим к ней два метода. Клиентское приложение Рис. 14.7. Приложение, реализующее распределенную транзакцию
652 Глава 14. Службы компонентов Первый из методов назовем Add_Order. Параметры этого метода будут следую- щими: ж Addr типа WideString — адрес заказчика, полученный из клиентского приложе- ния; ж Orderedltem типа WideString — наименование заказанного товара; ж UmtPrice типа Currency — цена единицы заказанного товара; Ж Quantity типа Integer — количество единиц заказанного товара. Реализация этого метода имеет вид: procedure TOrders_Data.Add_Order(const Addr, Orderedltem: WideString: UnitPrice: Currency: Quantity: Integer): var 01, Add: String: begin try // Соединяемся с базой данных ADOCOnnect i on1.Open: 11 Если в текстовых данных есть кавычки, И заменяем их на две. чтобы удовлетворить синтаксису И запросов, включающих такие данные 01 := QuotedStr(Orderedltem): Add := QuotedStr(Addr): // Текст запроса к базе данных ADOCommandl.CommandText := INSERT INTO OrderedProducts ' + ’ VALUES!’ + Add + ’. ' + 01 + '. ' + FormatFloatl'#####"."##',UnitPrice*100) + ’ + IntToStr(Quantity) + // Выполняем запрос ADOCommandl.Execute: // Разрываем соединение с базой данных ADOConnecti onl.Close: // Информируем службы компонентов об успешном // выполнении запроса SetComplete; except И Разрываем соединение с базой данных ADOCOnnecti onl.Close; И Информируем службы компонентов о неудачной попытке II выполнения запроса SetAbort; // Передаем исключение вызывающему приложению или объекту raise; end; end:
Управление транзакциями 653 Второй метод, Get_Order_List, возвращает набор данных, являющийся резуль- татом запроса к таблице OrderedProducts: function TOrders_Data.Get_Order_List: Recordset: var QRY : String: begin // Текст запроса QRY := 'SELECT * FROM OrderedProducts ORDER BY Ord_ID': try // Создаем набор данных Result := CoRecordset.Create: Result.CursorLocation := adUseClient: // Открываем его Result.Open(QRY. ADOConnectionl.Connect!onString. adOpenStatic. adLockBatchOptimistic. adCmdText): // Разрываем соединение с базой данных ADOCOnnecti onl.Close: // Информируем службы компонентов об успешном И выполнении запроса SetComp1ete: except ADOConnecti onl.Close: // Информируем службы компонентов о неудачной попытке И выполнения запроса SetAbort; // Передаем исключение вызывающему приложению raise: end: end: Как и в предыдущем случае, нам следует сослаться в секции uses на модули ADODB, AD0DBJLB и ADORJLB. После компиляции и сохранения проекта скопируем созданную нами библио- теку ORDERS.DLL на серверный компьютер и зарегистрируем объект Orders Data в службах компонентов в том же приложении СОМР1 us_Demo, что и предыдущий компонент. Создав два объекта СОМ+, манипулирующих двумя таблицами, приступим к созданию третьего объекта, выступающего в роли родительского по отношению к уже созданным двум объектам. Для этого создадим новую библиотеку ActiveX (назовем ее PROC), а в ней — новый объект СОМ+ (назовем его Processing). В от- личие от предыдущих двух объектов, этот объект будет наследником не класса TMtsDataModule, а класса TMtsAutoObject (для его создания следует выбрать значок Transactional Object на странице ActiveX окна репозитария объектов), поскольку дан- ный объект СОМ+ не будет содержать никаких компонентов доступа к данным. При создании этого объекта в качестве значения свойства Transaction Support нам следует выбрать пункт Requires a new transaction в списке Transaction model. Это означает, что при обращении к данному объекту для него создается собст-
654 Глава 14. Службы компонентов венный контекст транзакции, и он не может выполняться в контексте транзак- ции другого объекта. Напомним, что два ранее созданных объекта имели другое значение этого параметра, Requires a transaction, что позволяло им выполняться в контексте транзакции объектов, которые к ним обращаются. Добавим к библиотеке типов вновь созданного объекта три метода. Два из них, Get_OrderLi st и Get_ProductList, возвращают наборы данных, являющиеся ре- зультатами запросов к таблицам OrderedProducts и Products соответственно. Как и в ранее созданных объектах, оба метода проще всего создать, описав два свойства с атрибутом «только для чтения». Назначение этих методов — дать пользователю возможность визуально контролировать, что происходит с данными. Реализация этих методов имеет вид: function TProcess.Get_Order_List: Recordset; begin try // Создаем экземпляр контекста объекта Orders_Data 01eCheck(ObjectContext.CreateInstance(CLASS_Orders_Data. IOrders_Data. FOrders_Data)): // Создаем набор данных Result := CoRecordset.Create; Result.CursorLocation := adUseClient; // Получаем данные от объекта Orders_Data Result := FOrders_Data.Get_Order_List; // Информируем службы компонентов об успешном И выполнении запроса SetComplete; except // Информируем службы компонентов о неудачной попытке // выполнения запроса SetAbort; // Передаем исключение вызывающему приложению raise: end: end; function TProcess.Get_Product_List: Recordset: begin try // Создаем экземпляр контекста объекта Stock_Data 01 eCheck(ObjectContext.CreateInstance(CLASS_Stock_Data, IStock_Data. FStock_Data)): // Создаем набор данных Result := CoRecordset.Create: Result.CursorLocation := adUseClient: // Получаем данные от объекта Stock_Data Result := FStock_Data.Get_ProductList: // Информируем службы компонентов об успешном И выполнении запроса
Управление транзакциями 655 SetComplete: except И Информируем службы компонентов о неудачной попытке И выполнения запроса SetAbort; // Передаем исключение вызывающему приложению raise; end: end; В приведенном выше фрагменте кода мы используем метод Createlnstance контекста вызываемого объекта вместо создания самого объекта. Это позволяет нам использовать объекты именно тогда, когда они действительно нужны, и тем самым экономить ресурсы операционной системы. Чтобы заставить эти методы работать корректно, следует описать переменные, содержащие ссылки на вызы- ваемые объекты: private FOrders_Data: IOrders_Data; FStock_Data: IStock_Data; Помимо этого, следует сослаться на библиотеки типов ранее созданных объектов. Проще всего скопировать файлы PROC.TLB, PROC_TLB.PAS, STOCK.TLB и STOCK_TLB.PAS в каталог с текущим проектом и включить ссылку на модули PROC_TLB и STOCK_TLB в предложение uses наряду со ссылками на модули ADODB, ADODB_TLB и ADOR_TLB. Третий метод, Process_Order, будет реализовывать транзакцию, которая одно- временно модифицирует обе таблицы Product и OrderedProduct (или отменяет все изменения, если во время ее работы произошло исключение). Параметры этого метода: » Prod_ID типа Integer — первичный ключ модифицируемой записи таблицы Products; » Prod_Name типа WideString — наименование заказанного товара; ® UnitPrice типа Currency — цена единицы заказанного товара; Ж Quantity типа Integer — количество единиц заказанного товара. » Add г типа Wi deSt г 1 ng — адрес заказчика, полученный из клиентского приложения. Реализация метода ProcessOrder имеет вид: procedure TProcess.Process_Order(Prod_ID: Integer: const Prod_Name: WideString; UnitPrice; Currency: Quantity: Integer; const Addr: WideString); begin try // Создаем контекст объекта StockJJata 01 eCheck(Objectcontext.CreateInstance(CLASS_Stock_Data. IStock_Data. FStock_Data)); // Создаем контекст объекта OrdersJJata
656 Глава 14. Службы компонентов QleCheck(ObjectContext.CreateInstance(CLASS_OrdersJ)ata. IOrders_Data, FOrders_Data)); // Добавляем запись к таблице OrderedProducts FOrders_Data.Add_Order(Addr. Prod_Name. UnitPrice, Quantity); // Уменьшаем значение поля UnitsInStock FStock_Data.Dec_UnitsInStock(Prod_ID. Quantity): // Увеличиваем значение поля UnitsOnOrder FStock_Data.Inc_UnitsOnOrder(Prod_ID, Quantity); // Сообщаем службам компонентов. 11 что результат транзакции может быть сохранен Enabl eCormri t; except // Сообщаем службам компонентов. И что результат транзакции должен быть отменен DisableCommit: // Передаем клиенту исключение raise; end; end: Некоторые из строк приведенного выше фрагмента кода требуют отдельных пояснений. После создания контекстов дочерних объектов мы вызываем их ме- тоды. В действительности наиболее «опасным» с точки зрения возникновения исключения является следующий вызов: FStock_Data.Dec_UnitsInStock(Prod_ID, Quantity); Причина этого заключается в возможном нарушении ограничения на значе- ние поля UnitsInStock таблицы Products — оно должно быть неотрицательным, а в методе из него вычитается целое число. При возникновении связанного с этим исключения результат выполнения предыдущего метода должен быть аннулиро- ван, и чуть позже мы в этом убедимся. Сохранив и скомпилировав проект, скопируем созданную библиотеку proc.dll на серверный компьютер и зарегистрируем в службах компонентов в том же приложении COMPlus_Denio, что и предыдущие два компонента (рис. 14.8). Теперь нам следует создать клиентское приложение для проверки механизма сохранения результата и отката транзакции, реализованной в созданном объекте. Это мы обсудим в следующем разделе. Тестирование транзакций Создадим новый проект клиентского приложения. На его главную форму помес- тим компонент TRDSConnection и два компонента TADODataSet. Свойство Serve г Name компонента RDSConnectionl установим равным программному идентификатору (ProgID) третьего из созданных нами компонентов (в данном случае proc. Process), а свойство ComputerName установим равным имени серверного компьютера. Как и в случае предыдущего клиентского приложения, никаких свойств компонентов TADODataSet устанавливать не нужно — на этапе выполнения эти компоненты получат наборы данных от объекта Process. Чтобы пользователь мог увидеть их
Управление транзакциями 657 содержимое, поместим на форму по два компонента TDataSource и TDBGrid и свя- жем их с соответствующим компонентом TADODataSet. Наконец, поместим на форму два компонента TEdit для ввода количества единиц заказанного товара и адреса клиента и две кнопки, инициирующие вызовы методов объекта Process. Рис. 14.8. Приложение СОМ+ для реализации транзакций Создадим обработчики события OnClick этих кнопок. Первый из них иници- ирует получение и отображение двух наборов данных, предоставленных объектом Process, а именно результатов запросов к таблицам Products и OrderedProducts: procedure TForml.ButtonlC11ck(Sender: TObject): begin try // Инициируем создание контекста объекта Process RDSConnecti onl. Connected := True; // Получаем список заказов RS := RDSConnectionl.GetRecordset('Order_List'. "); AD0DataSet2.Recordset := RS: AD00ataSet2.0pen: // Получаем список товаров RS := RDSConnectionl.GetRecordset('Product_List', ");
658 Глава 14. Службы компонентов ADODataSetl.Recordset := RS; ADODataSetl.Open; // Можно принимать заказы Button2.Enabled := True; except // Некоторые из объектов недоступны ShowMessage('Данные не получены'); Button2.Enabled := False; end; RDSConnectlonl.Connected := False; end: Для того чтобы приведенный выше фрагмент кода был работоспособным, сле- дует сослаться на модуль ADODB в секции uses и добавить описание переменной RS: var Forml: TForml: RS; _RecordSet; Второй обработчик события инициирует транзакцию, вызывая метод ProcessOrder: procedure TForml.Button2Click(Sender: TObject); begin try // Инициируем создание контекста объекта Process RDSConnectlonl.Connected := True: // Вызываем его метод ProcessJJrder RDSConnectlonl.AppServer.Process_Order( ADODataSet1.Fi eldByName('P roductID').AsInteger, ADODataSetl.FieldByName('ProductName').AsString, ADODataSetl.FieldByName('UnitPrice').AsFloat. StrToInt(Editl.Text),Edit2.Text): // Обновляем данные ButtonlClick(self): except // В одном из серверных объектов возникло исключение ShowMessage('Заказ не принят'); Button2.Enabled := False: end: RDSConnectlonl.Connected := False; end; Скомпилируем и сохраним проект, а затем скопируем полученное приложе- ние на клиентский компьютер. Выполним клиентское приложение. После щелчка на первой кнопке данные из таблиц Products и OrderedProducts будут отображены в компонентах TDBGrid. Далее мы можем выбрать строку в наборе данных из таблицы Products, ввести адрес и количество заказанного товара в соответствующих компонентах TEdit и щелкнуть на второй кнопке. Это приведет к выполнению транзакции. Если
Управление транзакциями 659 количество заказанных единиц товара не превышает значения поля UnitsInStock в выбранной записи, после обновления данных мы получим следующие резуль- таты (рис. 14.9): » значение поля UnitsInStock будет уменьшено на величину, введенную в поле Количество; Ж значение поля UnitsOnOrder будет увеличено на величину, введенную в поле Количество; й в таблице OrderedProducts появится новая запись. Рис. 14.9. Клиентское приложение для тестирования транзакций Следует отметить, что если при выполнении транзакции не произойдет ис- ключений, значение поля Ord_ID новой записи, которая появится в поле Ord_Id таблицы OrderedProducts, будет равно значению того же поля предыдущей записи плюс 1, что иллюстрируется приведенным рисунком. Однако при возникновении исключения ситуация будет иной. Попробуем ввести в поле Количество число, превышающее значение в поле UnitsInStock (то есть заказать товаров больше, чем есть на складе). В этом случае сначала объект Orders_Data добавит запись в таблицу OrderedProducts, а затем объект StockJData начнет выполнять свой метод DecJJnitsInStock, и в нем произойдет исключение, связанное с нарушением имею- щегося в базе данных ограничения на значение этого поля (на складе не должно быть отрицательного числа единиц товара). Произойдет откат транзакции, и вновь созданная запись, естественно, из таблицы будет удалена. Однако при этом текущее значение поля Ord_Id сохранится (напомним, что это поле типа Identity и его значения генерируются автоматически). Если затем с помощью этого же приложе- ния попытаться обработать другой заказ, удовлетворяющий ограничениям на зна- чение поля UnitsInStock, мы обнаружим, что в значениях поля Ord_Id появился
660 Глава 14. Службы компонентов пропуск — запись с предыдущим значением этого поля в таблице OrderedProducts отсутствует. Следовательно, откат предыдущей транзакции действительно про- изошел. Естественно, может возникнуть резонный вопрос: а зачем нужно было созда- вать эти три объекта, когда можно просто использовать механизм поддержки транзакций, предоставляемый самим сервером баз данных? Действительно, в слу- чае одной и той же базы данных (или даже разных баз данных, управляемых одинаковыми серверами) это не имеет особого смысла. Однако при использова- нии разнотипных серверов баз данных в этом есть прямой смысл — механизмы поддержки транзакций серверов баз данных обычно не поддерживают распре- деленные транзакции с участием СУБД других производителей, тем не менее с помощью служб компонентов такую поддержку можно организовать. Ниже мы обсудим, как это сделать. Управление распределенными транзакциями Для иллюстрации механизма реализации распределенных транзакций с помо- щью служб компонентов мы слегка модифицируем наше приложение. Теперь мы будем использовать две СУБД — Microsoft SQL Server 2000 и Oracle 8.0.4. Отметим, что использовать базы данных Oracle в распределенных транзакциях, основанных на применении объектов СОМ+, можно, начиная с Oracle 7.3.3. Именно эта версия Oracle является версией по умолчанию, описанной в ключе реестра: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSDTC\MTxOCI По умолчанию значения этого ключа таковы: OracleOciLib = "ociw32.dll" OracleXaLib = "xa73.dll" OracleSqlLib = "sqllibl8.dll" В случае применения Oracle 8 следует внести изменения в эти значения: OracleOciLib = "ociw32.dll" OracleXaLib = "xa80.dll" OracleSqlLib = "sql1ib80.dll" Отметим также, что распределенные транзакции с участием Oracle возможны при применении ODBC-драйвера, входящего в комплект поставки Microsoft Data Access Components, а не в комплект поставки Oracle 8. В случае применения Oracle 8i, помимо модификации ключей реестра, следует создать дополнительный сервис, предназначенный для поддержки распределен- ных транзакций. Интересующиеся этим вопросом могут обратиться к разделу «Using Microsoft Transaction Server with Oracle8i» документации Oracle. После этого краткого введения рассмотрим, как можно модифицировать при- веденный пример для реализации в нем распределенной транзакции (описанные ниже действия соответствуют применению Microsoft SQL Server 2000, Oracle 8.0.4, ODBC-драйверов из состава Microsoft Data Access Components и OLE DB Provider for ODBC drivers).
События СОМ+ 661 Для начала скопируем таблицу Products из базы данных Northwind в базу дан- ных Oracle (в нашем примере это стандартная база данных, которую можно сге- нерировать при установке Oracle). Сделать это можно, например, с помощью служб трансформации данных (Microsoft Data Transformation Services). При переносе данных лучше переименовать таблицу Products в PRODUCTS — это позво- лит нам не модифицировать тексты запросов в коде объекта Stock Data. Помимо этого, нам следует для этой таблицы создать серверное ограничение, аналогичное ограничению, имевшемуся в базе данных Northwind. Для этого сле- дует с помощью утилиты SQL Plus выполнить следующее SQL-предложение: ALTER TABLE SCOTT.PRODUCTS ADD CONSTRAINT PRODUCTSCHECKCONSTRAINT1 CHECK (UNITSINSTOCK>-1) Далее следует описать ODBC-источник для базы данных Oracle (это делается с помощью приложения Data Sources раздела Administrative Tools панели управле- ния Windows). Назовем его orcl_odbc. Теперь нам следует изменить наш объект Stock_Data. Все модификации заклю- чаются в изменении свойства Connectionstring компонента ADOConnectionl: Provider=MSDASQL.1: Password=MANAGER: Persist Security Info=True: User ID=SYSTEM: Data Source=orcl_odbc Теперь можно поместить новую версию библиотеки STOCK.DLL на серверный компьютер и запустить клиентское приложение. Мы можем убедиться, что и в этом случае распределенные транзакции обрабатываются корректно. В частности, нару- шение серверного ограничения, описанного в Oracle, как и в предыдущем случае, приводит к откату транзакции в Microsoft SQL Server, что подтверждается появ- лением «пропусков» в значениях первичного ключа таблицы OrderedProducts. Таким образом, с помощью служб компонентов мы можем организовать рас- пределенную транзакцию, в которой участвуют базы данных, управляемые СУБД разных производителей. События СОМ+ Механизм уведомления о событиях в службах компонентов Говоря об объектах СОМ+, нельзя не отметить механизм обработки событий, возникающих в таких объектах. В отличие от событий СОМ (о реализации которых мы уже говорили в главе 3), где за уведомлением «следит» сам сервер, в СОМ+ за событиями и уведомлением о них объектов-подписчиков «следят» службы компо- нентов. При этом для генерации событий создается отдельный объект, называемый издателем (publisher), к которому производится обращение в момент наступления
662 Глава 14. Службы компонентов события. После этого службы компонентов обращаются ко всем клиентам, под- писанным на это событие. Объекты-издатели событий СОМ+ не должны содержать реализации своих интерфейсов — реализация содержится не в серверном объекте, а в клиентском приложении-подписчике (subscriber), нуждающемся в уведомлении о данном со- бытии. Поэтому после описания интерфейсов компонент компилируется и реги- стрируется в службах компонентов. При наступлении события в объекте СОМ+ следует инициировать создание объекта, описывающего события СОМ+, и вызвать метод, соответствующий данному событию. В объекте-подписчике (это обычный COM-объект) обычно реализуется интерфейс объекта-издателя и его методы — в данном случае реализация методов играет роль обработчиков этих событий. Отметим, что и объект-издатель, и объект-подписчик должны быть зарегистри- рованы в одном и том же приложении СОМ+. Событием в объекте СОМ+ может быть наступление любого факта (например, в рассмотренном выше примере им может быть появление сотого заказа, заказ какого-то конкретного товара, попытка заказа товара в большем количестве, чем есть на складе и т. д.). В этом случае в коде объекта, инициирующего событие (в рассмотренном выше примере — в коде объекта Process), должен присутство- вать код, реализующий создание экземпляра объекта событий и вызов метода, соответствующего этому событию. Например, если объект событий называется MyEvent, интерфейс объекта событий — IMyEvent, а метод, уведомляющий о событиях, называется OrdMessage и обладает одним строковым параметром, то фрагмент кода, инициирующий создание события, связанного с попыткой заказа товара в коли- честве, превышающем имеющееся на складе, может выглядеть, например, так: type FMyEvent: IMyEvent: procedure TProcess.Process_Order(Prod_ID: Integer: const Prod_Name: WideString: UnitPrice: Currency: Quantity: Integer; const Addr: WideString): begin try // Создаем контекст объекта StockJJata 01 eCheck(Objectcontext.CreateInstance(CLASS_Stock_Data, IStock_Data, FStock_Data)); // Создаем контекст объекта OrdersJJata 01eCheck(0bjectContext.CreateInstance(CLASS_0rders_Data. IOrders_Data, FOrders_Data)): // Добавляем запись к таблице OrderedProducts FOrders_Data.Add_Order(Addr, Prod_Name, UnitPrice. Quantity): // Уменьшаем значение поля UnitsInStock FStock_Data.Dec_UnitsInStock(Prod_ID. Quantity): // Увеличиваем значение поля UnitsOnOrder FStock_Data.Inc_UnitsOnOrder(Prod_ID, Quantity): // Сообщаем службам компонентов, 11 что результаты транзакции могут быть сохранены EnableCommit:
События СОМ+ 663 except // Информируем службы компонентов о неудачной попытке И выполнения транзакции и генерируем событие, о котором И СОМ+ уведомит приложение, отвечающее за доставку И товара на склад 01 eCheck(Objectcontext.Createlnstance(CLASS_MyEvent. IMyEvent, FMyEvent)); FMyEvent.OrdMessage ('Товар ' + Prod_Name + 'нужен в количестве ' + IntToStr(Quantity)+ ' упаковок'); 01sableCommlt: // Передаем клиенту исключение raise; end: end: Отметим, что экземпляр объекта событий может быть создан и из клиентского приложения. Создание объекта-издателя Создадим простейший код, иллюстрирующий применение механизма обработки событий СОМ+. В случае Delphi 6 для его создания требуется установить первый и второй пакеты обновления (update packs; эти пакеты обновления доступны заре- гистрированным пользователям Delphi на web-сайте компании Borland Software Corporation) — исходная версия Delphi 6 содержала ряд ошибок, связанных с гене- рацией библиотек типов объектов событий и исправленных во втором пакете обнов- ления. В случае Delphi 7 для создания объектов-издателей пакеты обновления не требуются. Наш пример будет состоять из трех частей: объекта-издателя, генерирующего одно событие с одним строковым параметром, объекта-подписчика данного собы- тия (он будет просто выводить диалоговое окно с содержимым этого параметра), и клиентского приложения, инициирующего событие. Начнем с разработки объекта-издателя событий. Для этой цели создадим но- вую библиотеку ActiveX и создадим в ней объект-издатель, выбрав значок СОМ+ Event Object на странице ActiveX окна репозитария объектов (присвоим событию имя EVT, а самому объекту — EVT_events). В результате будет сгенерирована биб- лиотека типов, содержащая интерфейс IEVT, к которому можно добавлять методы, соответствующие различным событиям. Методов у интерфейса объекта-издателя может быть несколько. Обязатель- ным требованием к этим методам является то, что они должны возвращать зна- чение типа HRESULT, а их параметры не должны иметь модификаторов. Пример добавления такого метода иллюстрирует рис. 14.10. Отметим, что при разработке объекта-издателя не следует создавать реали- зацию методов его интерфейса. Эти методы должны быть реализованы в объ- екте-подписчике — там реализация будет играть роль обработчиков событий. Поэтому после описания всех методов можно скомпилировать созданную библио- теку и перенести ее на серверный компьютер.
664 Глава 14. Службы компонентов Рис. 14.10. Библиотека типов объекта-издателя событий Далее объект-издатель следует зарегистрировать в службах компонентов. Рекомендуется делать это не средствами Delphi, а с помощью окна управления службами компонентов. Для этой цели создадим в службах компонентов новое приложение С0М+ (назовем его event demo) и выберем в контекстном меню раз- дела Components созданного приложения команду New ► Component. После этого будет запущен мастер COM Component Install Wizard (рис 14.11), в окне которого следует щелкнуть на кнопке Install new event class(es). Welcome to the COM Component IhstaB: Impoil of Install a Component Please choose whether you want to install a new component or install components that are already registered. evert_demo My Computer Application: Computer Рис. 14.11. Регистрация объекта-издателя событий в службах компонентов
События СОМ+ 665 Далее следует выбрать файл EVT_event.dll, а затем выделить те интерфейсы или отдельные события, на которые могут быть подписаны будущие подписчики (в данном случае, как показано на рис. 14.12, это единственное событие Eventl). Рис. 14.12. Выбор публикуемых событий Таким образом, мы зарегистрировали объект-издатель в службах компонен- тов. Нашей следующей задачей будет создание объекта-подписчика. Объект-подписчик представляет собой обычный внутрипроцессный СОМ- сервер, реализующий интерфейс объекта-издателя. В Delphi 7 для создания под- писчиков имеется специальный мастер, однако создать его можно и с помощью Delphi 6 — в этой версии Delphi мастер создания подписчиков отсутствует. Мы рассмотрим оба способа создания подписчиков. Создание объекта-подписчика с помощью Delphi 6 Создание подписчика с помощью Delphi 6 мы начнем с генерации новой библио- теки ActiveX. Далее на странице ActiveX окна репозитария объектов выберем зна- чок COM Object и в окне мастера COM Object Wizard укажем имя нового объекта (пусть он называется Subscrl). Поскольку нам следует реализовать готовый ин- терфейс, щелкнем на кнопке List, в появившемся окне щелкнем на кнопке Add Library, выберем созданную ранее библиотеку EVT_events.dll, а затем найдем и вы- берем в списке доступных интерфейсов интерфейс IEVT. Теперь нам осталось реализовать метод Eventl этого интерфейса. В исходном интерфейсе этот метод, как и все методы объектов событий, объявлен виртуаль- ным и абстрактным (это сделано для того, чтобы не позволить создать его реали- зацию в объекте событий). Поэтому первое, что следует сделать при создании
666 Глава 14. Службы компонентов кода объекта-подписчика, — удалить ключевые слова vi rtual и abstract из опре- деления метода. Теперь в окне редактора кода можно установить курсор на строку с определением метода Eventl, щелкнуть правой кнопкой мыши и вы- брать в контекстном меню команду Complete class at cursor. Реализация метода в нашем примере достаточно проста: мы выведем диагно- стическое сообщение на экран серверного компьютера: procedure TSubscr.Eventl(const Eventl_data: WideString); begin ShowMessage(Eventl_data): end; Отметим, что в общем случае действия, выполняемые подписчиком, могут быть практически любыми, и вывод диагностического сообщения — далеко не самый лучший (и, вообще говоря, не самый правильный) способ обработки таких событий в реальных проектах. Теперь нашу библиотеку можно сохранить (назовем ее subdl 1), скомпилиро- вать, перенести на серверный компьютер и установить в то же самое приложе- ние, что и предыдущий созданный объект. Поскольку за уведомление подписчика о событии отвечают службы компо- нентов, их следует проинформировать о том, что данный подписчик в них нуж- дается. Делается это путем создания подписки (subscription). Создать подписку можно, выбрав в контекстном меню раздела Subscriptions компонента-подписчика (в данном случае subdl 1 .Subscr) команду New ► Subscription. После этого будет запущен мастер COM New Subscription Wizard, в окне которого следует выбрать программный идентификатор объекта-издателя, о котором следует уведомлять наш объект-подписчик (рис. 14.13). Рис. 14.13. Выбор объекта-издателя для подписки
События СОМ+ 667 При описании свойств подписки можно указать, что она должна стать доступ- ной немедленно. Кроме того, свойства подписки (например, правила фильтра- ции событий, помещение уведомлений в очередь сообщений и т. д.) можно изме- нить, выбрав в контекстном меню соответствующего значка команду Properties. Состав созданного и сконфигурированного нами приложения СОМ+ иллю- стрирует рис. 14.14. Рис. 14.14. Состав приложения СОМ+ для тестирования событий Далее нам следует протестировать работу созданного приложения. Тестирование уведомлений о событиях Теперь нам осталось проверить, как осуществляются уведомления о событиях в службах компонентов. Для этой цели создадим приложение, которое будет ини- циировать генерацию событий. Это будет обычное Windows-приложение, создаю- щее экземпляр объекта-издателя и вызывающее его метод Eventsl. Создадим новый проект и добавим на главную форму создаваемого приложения компоненты TButton, TEdit и TRDSConnection. Значение свойства ServerName компо- нента TRDSConnection установим равным имени объекта события — EVT_events. evt. В качестве свойства ComputerName укажем имя серверного компьютера (при совпаде- нии серверного и клиентского компьютеров можно оставить это свойство пустым). Создадим обработчик события, связанного со щелчком на кнопке: procedure TForml.ButtonlClick(Sender: TObject): begin try
668 Глава 14. Службы компонентов RDSConnectionl.Connected : = True: RDSConnectionl.AppServer.Eventl(Editl.Text): except ShowMessaget'Событие не сгенерировано'): end; RDSConnectionl.Connected := False: end: Теперь скомпилируем и запустим проект. Если ввести в компонент Editl строку и щелкнуть на кнопке, будет создан экземпляр объекта-издателя и произведено обращение к его методу Eventl, после чего службы компонентов создадут экземп- ляр объекта-подписчика и вызовут его метод с таким же именем и с тем же зна- чением параметра. В результате приложение dllhost.exe, в адресном пространстве которого выполняется подписчик, выведет на экран серверного компьютера диа- логовое окно с введенной в клиентском приложении строкой (рис. 14.15). We Опубликовать have а VIР customer Рис. 14.15. Тестирование уведомлений о событиях Создание объекта-подписчика с помощью Delphi 7 Создание подписчика с помощью Delphi 7 мы также должны начать с генерации новой библиотеки ActiveX. Далее на странице ActiveX окна репозитария объектов выберем значок СОМ+ Subscription Object (рис. 14.16). Рис. 14.16. Создание объекта-подписчика с помощью Delphi 7
События СОМ+ 669 В окне мастера СОМ+ Subscription Object укажем имя нового объекта (пусть он называется Subd7). Поскольку нам следует реализовать готовый интерфейс, щелк- нем на кнопке Browse (рис. 14.17). ОМ ♦ Рис. 14.17. Окно мастера СОМ+ Subscription Object Сахе! В появившемся диалоговом окне СОМ+ Event Interface Selection выберем ин- терфейс IEVT объекта EVT_events и щелкнем на кнопке ОК (рис. 14.18). Рис. 14.18. Выбор интерфейса для реализации его в объекте-подписчике Теперь нам осталось реализовать метод Eventl этого интерфейса. Как и в пре- дыдущем случае, мы выведем диагностическое сообщение на экран серверного компьютера: procedure TSubscr.EventKconst Eventl_data: WideString); begin ShowMessage(Eventl_data +
670 Глава 14. Службы компонентов #13#10'Subscriber is created with Delphi 7'): end: Теперь пашу библиотеку можно сохранить (назовем ее subdl 1), скомпилиро- вать, перенести на серверный компьютер, установить в то же самое приложение, что и предыдущие два объекта, и создать одну подписку для этого компонента, указав, что она должна быть доступна немедленно. Далее нам следует протестировать работу созданного приложения. Запустим созданное ранее приложение, инициирующее генерацию событий. Теперь, если ввести в компонент Editl этого приложения строку и щелкнуть на кнопке, будет создан экземпляр объекта-издателя и произведено обращение к его методу Eventl, после чего службы компонентов создадут экземпляры обоих объектов-подписчи- ков и вызовут метод Eventl каждого из них с тем же значением параметра. В ре- зультате приложение dllhost.exe, в адресном пространстве которого выполняется подписчик, поочередно выведет на экран серверного компьютера два диалоговых окна с введенной в клиентском приложении строкой, инициированных обоими подписчиками (рис. 14.19). dUhQSt This is a new Рис. 14.19. Тестирование объектов-подписчиков Таким образом, мы создали простейший объект событий и два объекта-под- писчика, протестировали их совместную работу и убедились на этом примере, что механизм нотификаций СОМ+ довольно удобен для применения на практике. Заключение В этой главе мы познакомились с вопросами создания приложений с помощью служб компонентов. Мы узнали, что: Я службы компонентов обеспечивают возможность коллективного использова- ния СОМ-объектов и ресурсов (таких как соединения с базами данных) мно- гими клиентами, а также возможность организации авторизованного доступа к объектам; Ж объекты СОМ+, управляемые службами компонентов, объединяются в при- ложения СОМ+;
Заключение 671 Ж с помощью объектов СОМ+ и сервиса Microsoft Distributed Transaction Co- ordinator можно реализовать транзакции, в том числе затрагивающие не- сколько баз данных; Я в состав Delphi Enterprise входит два класса для создания объектов СОМ + — класс TMtsAutoObject для создания объектов СОМ+, не содержащих компонен- тов доступа к данным, и класс TMtsDataModul е, представляющий собой модуль данных, реализующий интерфейс lAppServer, что позволяет создавать на его базе приложения СОМ+, одновременно являющиеся DataSnap-приложениями. Мы создали несколько приложений, иллюстрирующих процедуру создания объектов СОМ+, в том числе мы создали объекты, реализующие доступ к дан- ным и их обновление, а также объекты, реализующие транзакцию. Мы также об- судили, как реализовать распределенную транзакцию, затрагивающую базы дан- ных, управляемые серверами различных производителей, и убедились, что это довольно удобный способ создания приложений подобной функциональности. Мы обсудили механизм обработки событий, возникающих в объектах СОМ+. Мы узнали, что: Ж службы компонентов сами «следят» за событиями и уведомлением о них кли- ентов-подписчиков; Я для генерации событий создается отдельный объект-издатель, к одному из методов которого производится обращение в момент наступления события; Ж после обращения к издателю службы компонентов обращаются ко всем кли- ентам, подписанным на это событие, и инициируют его обработку этими клиентами. Мы создали пример, иллюстрирующий применение механизма обработки со- бытий СОМ+ и состоящий из объекта-издателя событий, объектов-подписчиков и клиентского приложения, инициирующего событие. Мы убедились на этом примере, что механизм нотификаций СОМ+ довольно удобен для практического применения.
Вместо заключения В нашей книге, посвященной разработке COM-приложений в Delphi, вы позна- комились с созданием различных типов СОМ-серверов и COM-клиентов и, в ча- стности, с созданием и применением элементов управления ActiveX, с использо- ванием в приложениях OLE-документов, с созданием контроллеров и серверов автоматизации, с разработкой модулей расширения приложений Microsoft Office, с организацией распределенной обработки данных с помощью технологии Data- Snap и собственных ASP-объектов, с применением в своих приложениях возмож- ностей, предоставляемых службами компонентов Microsoft. Технология СОМ позволяет создавать приложения с самыми необычными возможностями, и Delphi — один из лучших инструментов для этого. Надеемся, что смогли убедить вас в этом с помощью рассмотренных в этой книге примеров. Каково будущее СОМ? Это отнюдь не праздный вопрос, ведь технологии сейчас развиваются очень быстро. Нельзя не обойти вниманием тот факт, что COM-приложения полностью поддерживаются платформой Microsoft .NET. Это означает, что решать свои задачи с помощью СОМ можно и нужно, даже если вы уже в ближайшее время в своих разработках планируете ориентироваться на платформу Microsoft .NET — решения на основе СОМ и СОМ+, если они спро- ектированы и реализованы грамотно, прослужат вашим заказчикам и вашим пользователям очень долго. Мы искренне надеемся, что все, что вы прочли в этой книге, поможет вам в соз- дании таких решений, и желаем вам успехов в вашем нелегком труде. С уважением, Наталия Елманова, Сергей Трепалин, Анатолий Тенцер
ПРИЛОЖЕНИЕ Инструкция по использованию компакт-диска Для установки примеров скопируйте содержимое архива Examples.zip прилагае- мого компакт-диска в какой-либо каталог на жестком диске. Для успешного открытия всех примеров желательно установить в среду раз- работки Delphi пакет компонентов, который находится в каталоге ...Examples\ Components\COMBookD6.dpk. Для этого в меню Delphi выберите команду Com- ponent ► Install Packages и в открывшемся диалоговом окне щелкните на кнопке Add. Примеры разбиты по главам, например, в каталоге ...Examples\Chap1 находятся примеры к главе 1. В настоящем приложении приведены краткие сведения о тех примерах, кото- рые, помимо Delphi, требуют наличия дополнительного программного обеспече- ния (Microsoft SQL Server, Internet Information Services и т. д.) или специальной настройки операционной системы либо среды разработки Delphi, например ре- гистрация в ней COM-серверов или установка дополнительных компонентов. Здесь содержится краткая информация, необходимая для компиляции и запуска проекта. Отсутствие такой информации означает, что проект может быть ском- пилирован и запущен без какой-либо специальной настройки. При этом подра- зумевается, что на компьютере установлены пакеты Delphi 7 Enterprise Edition (полностью) и Microsoft Office 2000 или Microsoft Office ХР, а пользователь ком- пьютера имеет права администратора Windows. Отметим, что данное приложение содержит лишь минимальные сведения о про- ектах. Подробности следует искать в соответствующих главах книги. Глава 1 COMPIugins В первую очередь следует открыть и откомпилировать проект ...Chap1\COMPIugins\ ImpTxt.dpr, а затем выбрать в меню Delphi команду Run ► Register ActiveX Server. Глава 3 Перед тестированием всех примеров к данной главе следует запустить один раз исполняемый файл ...Chap3\Server\AutoServ.exe.
674 Приложение. Инструкция по использованию компакт-диска Перед тестированием проекта ...Chap3\CliNote\CliNote.dpr нужно выбрать ко- манду Project ► Import Type Library и зарегистрировать библиотеку AutoServ. Глава 4 Для выполнения примеров из этой главы требуется наличие установленных при- ложений Microsoft Word, Excel, PowerPoint, Outlook из комплекта поставки Microsoft Office 2000 или Microsoft Office XP. Word_Rpt Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Откройте проект ...04\Word_Rpt\Word_Rpt.dpr. На форме выделите компонент ADOConnectionl. В инспекторе объектов в свойстве Connect!onString фрагмент строки Data Source=MAINDESK следует заменить фрагментом Data Source=XXX, где XXX — имя компьютера, на котором установлена СУБД Microsoft SQL Server. При необходимости следует в этом же свойстве изменить имя пользователя и пароль, уточнив их у администратора Microsoft SQL Server. 2. Проверьте правильность введенных параметров, изменив свойство ADOConnecti onl. Connected на Тrue. При этом не должно быть исключений. 3. Скомпилируйте и запустите проект. Excel_Rpt Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Откройте проект ...Chap4\Excel_Rpt\Excel_Rpt.dpr. На форме выделите компо- нент ADOConnectionl. В инспекторе объектов в свойстве Connectionstring фраг- мент строки Data Source=MAINDESK следует заменить фрагментом Data Source=XXX, где XXX — имя компьютера, на котором установлена СУБД Microsoft SQL Server. При необходимости следует в этом же свойстве изменить имя пользователя и пароль, уточнив их у администратора Microsoft SQL Server. 2. Проверьте правильность введенных параметров, изменив свойство ADOConnecti onl. Connected на True. При этом не должно быть исключений. 3. Скомпилируйте и запустите проект. ExcelQueryT able Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Откройте проект ...Chap4\Excel_Rpt\Excel_Rpt.dpr. В коде приложения в строке Qt:=Ws.QueryTables.Add(... фрагмент SERVER=MAINDESK следует заменить фрагмен-
Г лава 5 675 том SERVER=XXX (XXX — имя компьютера, на котором установлена СУБД Microsoft SQL Server), фрагмент WSID=MAINDESK — фрагментом WSID=YYY (YYY — имя компью- тера, на котором выполняется приложение), а фрагмент UID=Admi nistrator — фрагментом UID=ZZZ (ZZZ — имя пользователя Windows, на котором выполня- ется приложение). Если параметры защиты сервера SQL Server не позволяют обращаться к нему пользователям Windows, следует изменить строку, указав имя пользователя SQL Server и пароль, предварительно уточнив их у адми- нистратора Microsoft SQL Server. 2. Скомпилируйте и запустите проект. IconExtractor Если пример IconExtractor будет запускаться в среде разработки, то необходимо выбрать в меню Delphi команду Tools ► Debugger Options и, как показано на ри- сунке, на вкладке Language Exceptions в диалоговом окне Debugger Options снять флажок Stop on Delphi Exceptions. Глава 5 Перед открытием проекта PCompon необходимо убедиться, что компонент TDBO1 eContainer зарегистрирован в среде разработки и находится на странице COMBookD6 палитры компонентов.
676 Приложение. Инструкция по использованию компакт-диска Глава 7 Перед запуском проектов ..Chap7\SafeCall\SafeCallClient\SafeCallClient.exe и ...Chap7\ SafeCall\StdCallClient\StdCallClient.exe следует по разу запустить проекты ...Chap7\ SafeCall\SafeCallServer.exe и ...Chap7\SafeCall\StdCallServer.exe для их регистра- ции в системном реестре. Перед запуском проекта ...Chap7\SimpleNotifications\NoteCli.exe следует от- крыть проект Notelnp.dpr и выбрать в меню Delphi команду Run ► Register ActiveX Server. Глава 8 Addins Для тестирования проекта ...Chap8\Addlns\SimpleAddln.dll необходимо иметь уста- новленный пакет Microsoft Office 2000 или Microsoft Office ХР. 1. Откройте и откомпилируйте проект ...Chap8\Addlns\SimpleAddln.dpr и в меню Delphi выберите команду Run ► Register ActiveX Server. 2. Закройте все приложения Microsoft Office. 3. Откройте Microsoft Word и щелкните на кнопке Dir, как показано на рисунке. SmartTags Для тестирования проекта ...Chap8\SmartTag\st.dll необходимо иметь установ- ленный пакет Microsoft Office ХР. При его отсутствии генерируется ошибка в процессе загрузки библиотеки типов при открытии в Delphi проекта ...Chap8\ SmartTag\st.dpr. Глава 9 ShellFolder Для тестирования проекта ...Chap9\ShellFolder\ShellFolder.dll необходимо от- крыть проект ...Chap9\ShellFolder\ShellFolder.dpr и в меню Delphi выбрать ко-
Глава 11 677 манду Run ► Register ActiveX Server. После этого рекомендуется перезагрузить компьютер. Далее следует открыть Windows Explorer и щелкнуть на значке Inprise Fish Viewer. DirChangeHook Для тестирования проекта ...Chap9\DirChangeHook\WEHook.dll необходимо от- крыть проект ...Chap9\DirChangeHook\WEHook.dpr и в меню Delphi выбрать ко- манду Run ► Register ActiveX Server. После этого рекомендуется перезагрузить компьютер. AutoComplete Перед открытием проекта ...Chap9\AutoComplete\Test.dpr необходимо убедиться, что компонент AcEdlt зарегистрирован в среде разработки на странице ComBookD6 палитры компонентов. WebBrowser Перед открытием проекта ...Chap9\WEBBrowser\WEBBrowser.dpr необходимо убе- диться, что компоненты AcEdlt и CustomizedWEBBrowser зарегистрированы в среде разработки на странице ComBookD6 палитры компонентов. Для корректного теста желательна русифицированная версия Windows. PropertySheet Перед тестированием библиотеки ...Chap9\PropertySheet\TxtSheet.dll необходимо откомпилировать проект ...Chap9\PropertySheet\TxtSheet.dpr и выбрать команду Run ► Register ActiveX Server. Глава 11 DCOM_Controller Убедитесь, что на компьютере установлено приложение Microsoft Excel, и запус- тите проект ...Chap11\DCOM_Controller\cln.dpr. Socketcontroller Для тестирования этого проекта необходимо проделать следующую процедуру. 1. Убедитесь, что на компьютере установлено приложение Microsoft Excel. 2. Запустите приложение ScktSrvr.exe, находящееся в каталоге ...Delphi7\Bin. 3. Щелкните правой кнопкой мыши на значке Borland Socket Server панели за- дач и в контекстном меню выберите команду Properties. Далее раскройте меню Connections и снимите флажок у команды Registered Objects Only, как показано на рисунке ниже. 4. Остановите и снова запустите приложение ScktSrvr.exe.
678 Приложение. Инструкция по использованию компакт-диска 5. Откройте проект ...Chap11\Socket_Controller\cln.dpr. На главной форме проекта выделите компонент SocketConnectionl и в инспекторе объектов измените свойство Address на IP-адрес вашего компьютера либо очистите свойство Address и в качестве значения свойства Host введите имя вашего компьютера. 6. Запустите проект ...Chapl 1\Socket_Controller\cln.dpr. Intercept Для тестирования этого проекта необходимо проделать следующую процедуру. 1. Запустите приложение ScktSrvr.exe, находящееся в каталоге ...Delphi7\Bin. 2. Откройте проект ...Chapl 1\lntercept\Datalntr.dpr и в меню Delphi выберите ко- манду Run ► Register ActiveX Server. 3. Щелкните правой кнопкой мыши на значке Borland Socket Server панели за- дач и в контекстном меню выберите команду Properties. В появившемся диа- логовом окне, как показано на рисунке, в поле InerceptGUID введите следую- щее значение GUID: {616406CO-C199-11D5-94D6-OOOOE8625C26} 4. Запустите один раз проект ...Chapl 1\lntercept\MidServ.exe для регистрации в системном реестре.
Глава 11 679 5. Откройте проект ...Chap11\lntercept\Pcli.dpr, на главной форме проекта выбе- рите компонент SocketConnectionl и в инспекторе объектов измените свойство Address на IP-адрес вашего компьютера либо очистите поле Address и в качестве значения свойства Host введите имя вашего компьютера. 6. Запустите проект ...Chapl 1\lntercept\Pcli.dpr. Object_Broker Потребуется как минимум два компьютера, объединенных в сеть, на которых ус- тановлено приложение Microsoft Excel. 1. На каждом из компьютеров запустите приложение ScktSrvr.exe, находящееся в каталоге ...Delphi7\Bin. 2. На каждом из компьютеров щелкните правой кнопкой мыши на значке Borland Socket Server панели задач и в контекстном меню выберите команду Properties. Далее раскройте меню Connections и снимите флажок у команды Registered Objects Only. 3. Остановите и снова запустите приложение ScktSrvr.exe на каждом из компью- теров. 4. Откройте проект ...Chapl 1\Object_Broker\cln.dpr, выберите на форме проекта компонент ObjectBroker и откройте редактор свойства Servers. Удалите суще-
680 Приложение. Инструкция по использованию компакт-диска ствующую коллекцию серверов и определите имена двух ваших компьюте- ров, объединенных в сеть. 5. Запустите проект ...Chapl 1\Object_Broker\cln.dpr. Глава 12 lnteractive_Clients Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Откройте проект ...Chapl2\lnteractive_Clients\Server\Serv.dpr. На форме с име- нем RDM выделите компонент ADOConnectionl. В инспекторе объектов в свойстве Connectionstring фрагмент строки Data Source=TREPA следует заменить фрагментом Data Source=XXX, где XXX — имя компьютера, на котором установлена СУБД Micro- soft SQL Server. При необходимости следует в этом же свойстве изменить имя пользователя и пароль, уточнив их у администратора Microsoft SQL Server. 2. Проверьте правильность введенных параметров, изменив значение свойства ADOConnectionl.Connected на True. При этом не должно возникать исключений. Далее следует снова установить это значение равным False. 3. Скомпилируйте проект и запустите его для регистрации в системном реестре. 4. Откройте проект ...Chapl2\lnteractive_Clients\Client\Serv.dpr, скомпилируйте и запустите его. NTSvc Для тестирования этого проекта необходимо проделать следующую процедуру. 1. Откройте проект ...Chapl2\NTSvc\MidSvc.dpr, скомпилируйте его и запустите для регистрации сервера в системном реестре. 2. Выберите в меню Windows команду Start ► Run и зарегистрируйте сервис, введя в командной строке следующую команду (в ней нужно указать полный путь к серверу): ...Chapl2\NTSvc\MidSvc.exe /install 3. Перезагрузите компьютер. 4. Откройте проект ...Chapl2\NTSvc\PCIi.dpr, скомпилируйте его и запустите на выполнение. DataSnap Сервер Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Откройте проект ...Chapl2\DataSnap\MidServ.dpr и откройте модуль UServ2.pas в редакторе кода. Найдите следующий фрагмент:
Глава 13 681 const Connect! onStri ng='Provi der=SQLOLEDB. 1: Password's; Persi st Securi ty Info=True;llser ID=%s:Initial Catalog=Northwind;Data Source=TREPA'; 2. Замените в этом фрагменте имя TREPA именем компьютера, на котором уста- новлена СУБД Microsoft SQL Server. 3. Скомпилируйте проект и запустите его для регистрации в системном реестре. 4. Запустите приложение ...Delphi7\bin\scktsrvr.exe. Клиент Перед тестированием клиента необходимо отладить, скомпилировать и зарегист- рировать сервер, как это было описано выше. 1. Откройте приложение ...Chapl2\DataSnap\Client.dpr и выделите компонент SocketConnectionl на форме. В инспекторе объектов измените свойство Address на IP-адрес компьютера, на котором находится сервер доступа к данным (см. выше). Можно также очистить свойство Address и вместо него установить свойство Host равным имени компьютера, на котором находится сервер доступа к данным. 2. Запустите приложение. Активная форма Перед тестированием клиента необходимо отладить, скомпилировать и зарегистри- ровать сервер, как это было описано выше. На компьютере, где расположен клиент, либо на одном из компьютеров, доступных в локальной сети, должен быть запущен сервер Microsoft Internet Information Server или какой-либо другой web-сервер. 1. Откройте приложение ...Chap12\DataSnap\AXCIient.dpr и выделите компонент SocketConnectionl на форме. В инспекторе объектов измените свойство Address на IP-адрес компьютера, на котором находится сервер доступа к данным (см. выше). Можно очистить свойство Address и вместо него установить свойство Host равным имени этого компьютера. 2. Выберите команду Project ► WEB Deployment Options и в появляющемся диа- логовом окне измените значение в поле Target URL, указав IP-адрес вашего компьютера. При необходимости следует изменить значения в полях Target Dir и HTML dir — об этом проконсультируйтесь с администратором web-сервера. 3. Выберите команду Project ► WEB Deploy. 4. Запустите Microsoft Internet Explorer и наберите ссылку http://xxx.xxx.xxx.xxx/ AXClient.htm, где ххх.ххх.ххх.ххх — IP-адрес компьютера, указанный в поле HTML dir диалогового окна WEB Deployment Options. Если HTML-страница была помещена не в каталог, заданный по умолчанию, эта ссылка может выглядеть по-другому (подробности можно найти в тексте книги). Глава 13 Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind.
682 Приложение. Инструкция по использованию компакт-диска На компьютере, где тестируется ASP-проект, должен быть запущен сервер Micro- soft Internet Information Server или Microsoft Internet Information Services (IIS). 1. Откройте проект ...Chap13\ASP\ASP01.dpr. На форме с именем DataModulel выде- лите компонент ADOConnectionl. В инспекторе объектов в свойстве Connectionstring фрагмент строки Data Source=TREPA следует заменить фрагментом Data Source=XXX, где XXX — имя компьютера, на котором установлена СУБД Microsoft SQL Server. При необходимости следует в этом же свойстве изменить имя пользователя и пароль, уточнив их у администратора Microsoft SQL Server. 2. То же самое сделайте с компонентом ADOConnectionl, который располагается в модуле DataModule2. 3. Скомпилируйте проект и в меню Delphi выберите команду Run ► Register ActiveX Server. 4. Скопируйте файл ...Chap13\ASPTest\Finance в корневой каталог диска С:. 5. Скопируйте остальные файлы из каталога ...Chap13\ASPTest в каталог, дос- тупный для чтения внешним пользователям, обращающимся к IIS. По умол- чанию это каталог C:\lnetPub\WWWRoot, но он может быть изменен в процессе администрирования IIS. 6. Далее следует открыть в каком-либо текстовом редакторе файл Name.htm из каталога, в который на предыдущем шаге были скопированы файлы, и найти в нем следующую строку: http://192.168.0.2/Test/Test.asp 7. Найденную строку следует заменить другой (здесь ххх. ххх. ххх. ххх — IP-адрес компьютера, на котором расположен сервер IIS): http://ххх.ххх.ххх.xxx/Test.asp 8. Ту же операцию следует повторить с файлом Picture.htm. 9. Для тестирования проекта можно запустить Internet Explorer и ввести сле- дующие URL-адреса: http://ххх.ххх.ххх.xxx/Testl.asp http://ххх.ххх.ххх.ххх/Тest2.asp Глава 14 Transactions Для выполнения этого примера требуется доступ к СУБД Microsoft SQL Server и к входящей в комплект ее поставки базе данных Northwind. 1. Создайте в базе данных North wind пустую таблицу, выполнив следующий SQL-запрос: CREATE TABLE [dbo].[OrderedProducts] ( [Ord_ID] [int] IDENTITY (1. 1) NOT NULL.
Глава 14 683 [Address] [char] (40) NULL. [OrderedItem] [char] (50) NULL. [UnitPrice] [money] NULL. [Quantity] ) ON [PRIMARY] [int] NULL 2. В модуле данных проекта ...Chap14\Transactions\Source\Stock\stock.dpr выберите компонент ADOConnectionl. В инспекторе объектов в свойстве Connectionstring фрагмент строки Data Source=MAINDESK следует заменить фрагментом Data Source=XXX, где XXX — имя компьютера, на котором установлена СУБД Micro- soft SQL Server. При необходимости следует в этом же свойстве изменить имя пользователя и пароль, уточнив их у администратора Microsoft SQL Server. 3. То ж самое следует сделать с проектом ...Chap14\Transactions\Source\Orders\ orders, dpr. 4. Скомпилируйте все проекты, входящие в группу. 5. Создайте новое приложение в службах компонентов. Перетащите мышью файлы ...Chap 14\Тransactions\Source\Stock\stock.dll, .. .Chapl4\Тransactions\Source\Process\ proc.dll и ...Chap14\Transactions\Source\Orders\orders.dll из соответствующих ката- логов проектов в раздел с созданным приложением. 6. Далее можно запускать клиентские приложения ...Chapl4\Transactions\Source\ test_stock_client\teststock.exe и ...Chapl4\Тransactions\Source\all_client\allcln.exe. Подробности, связанные с установкой свойств приложения СОМ+, можно найти в главе 14. Events В данном случае важен порядок компиляции и регистрации библиотек. 1. Откройте проект ...Chap14\Events\Source\Publisher\EVT_Events.dpr. Выберите в меню Delphi команду Run ► Register ActiveX Server. Далее зарегистрируйте полученную библиотеку DLL в службах компонентов так, как это описано в главе 14. Команду Run ► Install СОМ+ Object выполнять не нужно. 2. Откройте проект ...Chap14\Events\Source\Subscriber\SubDll.dpr, если исполь- зуется Delphi 6, или ...Chap14\Events\Source\Subscriber_d7\SubD7.dpr, если ис- пользуется Delphi 7. Выберите в меню Delphi команды Run ► Register ActiveX Server и Run ► Install COM+ Object, установив созданную библиотеку в то же приложение, что и предыдущую. 3. Далее можно скомпилировать и запустить проект ...Chap14\Events\Source\ Event_Generator\gen.dpr. Подробности, связанные с регистрацией и тестированием событий СОМ+, можно найти в главе 14.
Алфавитный указатель А abstract, ключевое слово, 666 ActiveX безопасность, 123 вывод сообщений, 71 динамическая инициализация, 127 использование, 92 навигация по web-страницам, 111 обработка событий в HTML- документах, 121 отличие от компонентов VCL, 91 получение информации о контейнере, 108 поставка через Интернет, 105 свойства, 110 создание, 49, 91 активных форм, 102, 535 клиента, 535 меню, 106 на основе VCL-компонентов, 93 страниц свойств, 99 спецификация, 93 AddRef, метод, 23 Alloc, функция, 78 Application, объект, 174, 605 as, оператор, 19, 151, 430 ASP, технология, 420, 600 AutoComplete, объект, 437 В Bookmarks, коллекция, 175 Briefcase, модель, 531 С Characters, коллекция, 175 Chart, объект, 189 ChartObjects, коллекция, 189 Charts, коллекция, 184 Clone, метод, 488 CLSID, идентификатор, 46, 88 CM_GETDATALINK, сообщение, 246 CM_RELEASE, сообщение, 321 CoCreatelnstance, функция, 46, 81 CoGetClassObject, функция, 45 Collapse, метод, 180 СОМ, технология, 17 СОМ+, технология, 638 ConvertToTable, метод, 182 CoTaskMemAlloc, функция, 80 CoTaskMemFree, процедура, 80 CreateComObject, функция, 48 Createlnstance, метод, 44 CreateObject, метод, 234 CreateObjectFromFile, метод, 234 CreateOleObject, функция, 71 CreateProcess, функция, 74 CreateSemaphore, функция, 286 D DataSnap, технология, 518, 522, 565 DCE, технология, 51 DCOM, технология, 493 DDE, технология, 44, 386 DeleteCriticalSection, функция, 289 DestroyObject, метод, 234
Алфавитный указатель 685 DidAlloc, функция, 79 DLL динамическая загрузка, 304 обмен данными с приложением, 307 создание, 300 статическая загрузка, 304 DllCanUnloadNow, функция, 48 Documents, коллекция, 175 Е EAbstractError, исключение, 29 Edit, метод, 246 EnterCriticalSection, функция, 289 Execute, метод, 258 exports, секция, 307 F FillMemory, функция, 312 finalization, секция, 589 For Each, оператор, 485 FormStyle, свойство, 35 Free, процедура, 79 G GetldsOfNames, метод, 466, 470 GetPropertyString, метод, 110 GetPropertyStrings, метод, ПО GetPropertyValue, метод, ПО GetSize, функция, 79 GUID, идентификатор, 20, 21, 87, 91 н HeapMinimize, процедура, 79 http, протокол, 579 I lAmbientDispatch, интерфейс, 108 ICIassFactory, интерфейс, 44 IDataObject, интерфейс, 408 IDispatch, интерфейс, 49, 70, 95, 352, 466 IDL, язык, 51 IDropSource, интерфейс, 408 IDropTarget, интерфейс, 25 IDTExtensibility2, интерфейс, 351 lEnumFormatEtc, интерфейс, 408 I Enum Variant, интерфейс, 485 IID, идентификатор, 24, 95 IMalloc, интерфейс, 78, 80 IMarshal, интерфейс, 268 implementation, секция, 126, 591 implements, ключевое слово, 32 initialization, секция, 44, 56, 366, 589 InsertObjectDialog, метод, 234 interface, ключевое слово, 22 interface, секция, 128, 326 Invoke, метод, 467, 471 lOleClientSite, интерфейс, 129 lOleCommandTarget, интерфейс, 423 lOleControlSite, интерфейс, 129 lOlelnplaceSite, интерфейс, 129 IPerPropertyBrowsing, интерфейс, ПО IPersistFile, интерфейс, 387 IPersistPropertyBag, интерфейс, 116 IQueryPersistent, интерфейс, 483 is, оператор, 19 IShellFolder, интерфейс, 391 IShellLink, интерфейс, 387 IStream, интерфейс, 87 Items, коллекция, 205 ITypelnfo, интерфейс, 50 ITypeLibrary, интерфейс, 137 IUnknown, интерфейс, 22, 27, 624 L Language, свойство, 462 Layout, свойство, 199 LeaveCriticalSection, функция, 289 LRPC, технология, 132
686 Алфавитный указатель м MIDAS, технология, 518 Modified, свойство, 246 О OLE, технология, 44, 234 OnC lick, событие, 113 OnChange, событие, 162 OnClick, событие, 386 OnClose, событие, 162 OnDataChange, событие, 246 OnDestroy, событие, 139, 324 OnExecute, событие, 587 OnMouseMove, событие, 418 OnTerminate, событие, 260 OnUpdateData, событие, 246 Р Para graph, объект, 178 Paragraph, объект, 175 Paragraphs, коллекция, 175, 178 Pascal, язык программирования, 22 PersistPropertyBagLoad, метод, 120 PersistPropertyBagSave, метод, 120 PivotCache, объект, 191 PivotCaches, коллекция, 190 PivotTables, коллекция, 190 Presentation, объект, 198 Presentations, коллекция, 193 private, секция, 116,347 ProgID, идентификатор, 88 ProgIDToClassID, метод, 46 protected, секция, ПО, 125 public, секция, 343 Q Query Interface, метод, 21, 24 QueryTable, объект, 190 QueryTables, коллекция, 190 Quit, метод, 172 R Range, объект, 178 Realloc, функция, 78 Release, метод, 23, 87 Request, объект, 600 Reset, метод, 464 Response, объект, 601 Resume, метод, 258 Rows, коллекция, 182 RTTI, протокол, 468 S safecall, соглашение о вызовах, 68 Selection, объект, 180 Server, объект, 603 Session, объект, 603 Shapes, коллекция, 193 ShareMem, модуль, 19, 43 Shoxfc’Message, функция, 41 Slide, объект, 199 Slides, коллекция, 193, 198 stdcall, соглашение о вызовах, 126 Suspend, метод, 258 Synchronize, метод, 264 Т Table, объект, 182 Tables, коллекция, 181 TabOrder, свойство, 115 TActiveXControl, класс, 110 TApplication, объект, 326 TASPObject, класс, 611 TCanvas, класс, 296 TClientDataSet, компонент, 550, 579 TComponent, класс, 33 TConnectionBroker, компонент, 583 TDataSet, класс, 244 TDataSetProvider, компонент, 574 TDBDataSet, класс, 573 TDCOMConnection, компонент, 499 TextStyle, объект, 198
Алфавитный указатель 687 ThreadDone, метод, 261 Timeout, свойство, 462 TInterfacedObject, класс, 25 TLocalConnection, компонент, 584 ТМето, компонент, 133 TMtsAutoObject, класс, 639 TMtsDataModule, класс, 639 TObject, класс, 25, 28 TOIeContainer, компонент, 234 TryEnterCriticalSection, функция, 289 TScriptControl, компонент, 462 TSharedConnection, компонент, 584 TSimpleObjectBroker, компонент, 515 TSOAPConnection, компонент, 585 TSocketConnection, компонент, 505, 578 TStream, класс, 29 TThread, класс, 257 TThreadList, класс, 164 TVCLEnumerator, класс, 486 TVCLProxy, класс, 468 TVCLScriptControl, компонент, 489 TWebBrowser, компонент, 419 TWebConnection, компонент, 512, 579 и UNIX, операционная система, 256 URLMon, модуль, 114 uses, секция, 43, 56, 114 V VBScript, язык сценариев, 121 VCL, библиотека, 33 virtual, ключевое слово, 666 Visible, свойство, 172 W web-страницы навигация, 111 свойства элемента ActiveX, 116 смарт-теги, 370 WM_COMMAND, сообщение, 458 WM CREATED, сообщение, 457 WM DESTROY, сообщение, 458 WM DROPFILES, сообщение, 402 WM ICONNOTIFY, сообщение, 594 WM INITDIALOG, сообщение, 457 Workbooks, коллекция, 184 Worksheet, объект, 189 Worksheets, коллекция, 184 абзац, 175 абстрактный класс, 29 абстрактный метод, 29 автозавершение, 437 инициализация, 446 реализация, 444 создание компонента, 444 список истории, 439, 448, 450 тестирование, 450 управление, 442 функциональность, 437 целевая папка, 449 автоматизация, 464, 491 контроллер, 131 объект, 139 понятие, 131 приложений Microsoft Office Microsoft Excel, 183 Microsoft Outlook, 202 Microsoft PowerPoint, 192 Microsoft Word, 175 общие принципы, 172 сервер, 131 адресное пространство, 491 активация по месту, 234 сервера, 48 активная форма, 102, 535 создание, 102 тестирование, 103
688 Алфавитный указатель апартамент концепция, 266 многопоточный, 47, 266 нейтральный, 267 однопоточный, 47, 266 асинхронная развязка, 596 аутентификация пользователей, 553 Б база данных, 565, 615 безопасность, 463, 507, 637 библиотека визуальных компонентов, 33 динамически загружаемая, 17, 299, 491 типов, 49, 94, 136, 358, 552 бизнес-логика, 520 бизнес-правила, 549 битовая маска, 421 браузер, 91, 424, 537 брокер, 513 буфера обмен, 238 В взаимная блокировка, 292, 295, 549, 596 виртуальная таблица, 70 виртуальный каталог, 610 виртуальный метод, 28,110 внепроцессный сервер, 43, 131, 132, 491, 629 внутренние объекты, 175 внутрипроцессный сервер, 43, 55, 91, 132, 299, 331, 491 временной интервал, 462 временный поток, 510 временный файл, 243 Г главная форма, 330 главный модуль, 40 главный однопоточный апартамент, 270 главный поток, 84, 259, 264 глобальная переменная, 587 гость, 630 д двухфазное завершение транзакций, 636 демаршалинг, 267, 492 дескриптор окна, 99, 323 деструктор, 40, 445 диаграмма в отчете, 217 размер, 189 сводная, 170 создание, 189, 190 диалоговая функция, 456 диалоговое окно, 456 динамически загружаемая библиотека, 17, 299 динамический метод, 28 динамический обмен данными, 44, 386 диспетчер надстроек, 362 памяти, 78, 307 диспетчеризация, 132 диспинтерфейс, 72, 95 документ Microsoft Word вставка объектов, 178 текста, 178 закрытие, 176 открытие, 176 печать, 176 свойства, 182 создание, 176 сохранение, 176 таблицы, 181 документная модель объектов, 430 дочерний компонент, 472
Алфавитный указатель 689 дочерняя форма, 35, 328 дуальный интерфейс, 73, 335 3 задача, 209 заметка, 209 запись, 291 запрос, 543, 580 затенение методов, 126 захват процессора, 257 защищенный блок, 142, 396 защищенный доступ, 290 защищенный метод, 585 И идентификатор глобально уникальный, 20, 21, 132 интерфейса, 24, 95, 332 класса, 88, 132, 332 программный, 88, 175, 500 фабрики классов, 510 иерархия ASP-объектов, 600 классов, 28 объектов, 171 издатель понятие, 661 регистрация, 665 создание, 663 инициализация автозавершения, 446 критической секции, 288 окна, 457 свойств, 258 Интернет, 518 интерпретатор, 462 интерфейс lAmbientDispatch, 108 IClassFactory, 44 IDataObject, 408 IDispatch, 49, 70, 95, 352, 466 интерфейс (продолжение) I DropSource, 408 IDropTarget, 25 IDTExtensibility2, 351 lEnumFormatEtc, 408 lEnumVariant, 485 I Malloc, 78, 80 IMarshal, 268 lOleClientSite, 129 lOleCommandTarget, 423 lOleControlSite, 129 lOlelnplaceSite, 129 IPerPropertyBrowsing, 110 IPersistFile, 387 I PersistProperty Bag, 116 I Query Persistent, 483 IShellFolder, 391 IShellLink, 387 IStream, 87 ITypelnfo, 50 ITypeLibrary, 137 IUnknown, 22,27,624 интерфейсы дуальные, 73, 335 использование, 34 методы, 21 наследование, 20 нотификационные, 114 объявление, 22 отличие от классов, 20, 28 понятие, 20 реализация, 21, 25, 31 стандартные, 49 информационная система проблемы, 519 работа, 519 решение проблем, 521 состав, 518 информация о состоянии, 621 исключение, 29 исполняемый файл, 43, 491 история, 439
690 Алфавитный указатель К кисть, 296 класс Т ActiveXControl, 110 TASPObject, 611 TCanvas, 296 TComponent, 33 TDataSet, 244 TDBDataSet, 573 TInterfacedObject, 25 TMtsAutoObject, 639 TMtsDataModule, 639 TObject, 25, 28 TStream, 29 TThread, 257 TThreadList, 164 TVCLEnumerator, 486 TVCLProxy, 468 классы абстрактные, 29 иерархия, 28 наследование, 28 отличие от интерфейсов, 28 потокозащищенные, 296 клиент взаимодействие с сервером, 74 облегченный, 92, 492 создание, 60, 528, 535 тонкий, 92, 492 универсальный, 493 клон, 488 ключ первичный, 535 реестра, 129, 429 ключевое слово abstract, 666 implements, 32 interface, 22 virtual, 666 код ошибки, 338 коллекция, 152 Bookmarks, 175 Characters, 175 коллекция (продолжение) Chartobjects, 189 Charts, 184 Documents, 175 Items, 205 Paragraphs, 175, 178 PivotCaches, 190 PivotTables, 190 Presentations, 193 QueryTables, 190 Rows, 182 Shapes, 193 Slides, 193, 198 Tables, 181 Workbooks, 184 Worksheets, 184 компилятор, 26 компонент TClientDataSet, 550, 579 TConnectionBroker, 583 TDataSetProvider, 574 TDCOMConnection, 499 TLocalConnection, 584 TMemo, 133 TOIeContainer, 234 TScriptControl, 462 TSharedConnection, 584 TSimpleObjectBroker, 515 TSOAPConnection, 585 TSocketConnection, 505, 578 TVCLScriptControl, 489 TWebBrowser, 419 TWebConnection, 512, 579 компоненты визуальные, 33 вызов методов, 34 доступа к данным, 544 наследование, 34 службы, 632 чувствительные к данным, 246 конструктор, 21 контакты, 208 контейнер, 91, 234, 402, 588
Алфавитный указатель 691 контекст, 634 контроллер автоматизации, 131, 143, 171, 172, 491 критические секции, 140, 264, 288 кэш, 531 Л локальный файл, 541 м маршалинг, 20, 43, 55, 74, 137, 267, 491, 492, 558 маска, 421 массив, 436 менеджер ресурсов, 635 меню, 106 метод AddRef, 23 Clone, 488 Collapse, 180 ConvertToTable, 182 Createlnstance, 44 CreateObject, 234 CreateObjectFromFile, 234 DestroyObject, 234 Edit, 246 Execute, 258 GetldsOfNames, 466, 470 GetPropertyString, 110 GetPropertyStrings, 110 GetPropertyValue, 110 InsertObjectDialog, 234 Invoke, 467, 471 PersistPropertyBagLoad, 120 PersistPropertyBagSave, 120 ProgIDToClassID, 46 Queryinterface, 21, 24 Quit, 172 Release, 23, 87 Reset, 464 Resume, 258 метод (продолжение) Suspend, 258 Synchronize, 264 ThreadDone, 261 методы абстрактные, 29 асинхронные, 259 виртуальные, 28, 110 динамические, 28 затенение, 126 защищенные, 585 интерфейса, 21, 23 объекта автоматизации, 139 перекрытие, 29, 125 статические, 28, 489 экспонируемые, 156 многозадачность невытесняющая, 257 многопользовательская обработка данных, 532 многопоточный апартамент, 47, 266 многопоточный режим работы, 257 модальная форма, 318, 615 модель Briefcase, 531 многокомпонентных объектов, 17 модулей расширения, 350 объектная, 171 объектов документная, 430 потоков, 43, 54, 401 нейтральных, 267, 271 одного, 63, 271 разделенных, 271 свободных, 136,271 смешанных, 271 расширения компонента TScriptControl, 465 модули данных, 544 загрузка, 83 расширения, 38, 62, 105, 538 модель, 350 отладка, 362
692 Алфавитный указатель модули (продолжение) разработка, 358 реализация функциональности, 362 регистрация, 357 создание, 350 модуль ShareMem, 19, 43 URLMon, 114 мьютекс, 284 н набор данных, 580 ADO, 641 надстройка, 350 наследование, 20 невытесняющая многозадачность, 257 нейтральный апартамент, 267 немодальная форма, 323 несигнальное состояние, 272 нотификационные сообщения, 135, 159, 313, 343, 415, 558 нотификационный интерфейс, 114 О область видимости, 26 облегченный клиент, 92, 492, 522 обработка ошибок, 334 объект Application, 174, 605 AutoComplete, 437 Chart, 189 Paragraph, 175, 178 PivotCache, 191 Presentation, 198 QueryTable, 190 Range, 178 Request, 600 Response, 601 Selection, 180 Server, 603 объект (продолжение) Session, 603 Slide, 199 Table, 182 TApplication, 326 TextStyle, 198 Worksheet, 189 объектная модель, 171 Microsoft Excel, 184 Microsoft Office, 352 Microsoft Outlook, 203 Microsoft PowerPoint, 193 Microsoft Word, 175 объектно-ориентированное программирование, 28 объекты автоматизации, 139, 466 внедрение, 44 внутренние, 175 вставка в документ, 178 диспетчеризации, 132 доступа к данным, 646 иерархия, 600 класса, 81 коллекция, 152 не хранящие информацию о состоянии, 572, 573, 638 пул, 632 связывание, 44 серверные, 640 синхронизации, 279 событий, 280, 670 создание, 639 хранящие информацию о состоянии, 572 экспорт, 17 однонаправленный курсор, 583 однопоточный апартамент, 47, 266 оператор as, 19, 151, 318, 430 For Each, 485 is, 19,318
Алфавитный указатель 693 откат транзакции, 636 отчет, 210 очередь асинхронных вызовов процедур, 278 очистка стека, 301 пакет, 539 данных, 75 компонентов, 61 маршалинга, 75 обновления, 663 приложений, 639 программ, 17 память автоматическое управление, 22 выделение, 79 освобождение, 79 сжатие, 79 управление, 78 панель инструментов, 34 управления, 585 папка открытие, 204 создание, 204 первичный ключ, 535 перекрытие методов, 29, 125 перетаскивание, 396 источник данных, 407 контейнер, 402 перо, 296 печать документов Microsoft Word, 176 презентаций Microsoft PowerPoint, 195 рабочих книг Microsoft Excel, 186 подписка, 666 подписчик понятие, 662 создание, 665 подсчет ссылок, 22 позднее связывание, 70, 96, 146 полоса прокрутки, 428 полотно, 296 портфель, 531 поток временный, 510 выполнения, 43, 47, 257 главный, 84, 259, 264 единственный, 271 нейтральный, 271 приоритет, 259 разделенный, 271 свободный, 271 смешанный, 271 фоновый, 259, 264 данных, 29, 257 файловый, 542 потокозащищенный класс, 296 презентационная логика, 519 презентация закрытие, 195 открытие, 194 оформление, 196 печать, 195 создание, 194 сохранение, 195 приватные данные, 576 приведение типа, 27 проблемы традиционного программирования, 19 программный идентификатор, 88, 500 Microsoft Excel, 184 Microsoft Outlook, 203 Microsoft PowerPoint, 193 Microsoft Word, 175 прокси, 55, 75, 137, 267, 492 просмотр данных, 390 пространство имен, 464 протокол HTTP, 511,579 SSL, 511 TCP/IP, 503
694 Алфавитный указатель процедура CoTaskMemFree, 80 Free, 79 HeapMinimize, 79 процедуры деинициализации потока, 78 инициализации потока, 78 объявление, 55 удаленные, 76, 132 хранимые, 579 процессор регулярных выражений, 62 псевдоним, 527 пул объектов, 573, 632, 635 потоков, 47 ресурсов, 511,635,638 Р рабочая книга закрытие, 186 обращение к листам и ячейкам, 187 открытие, 185 печать, 186 создание, 185 сохранение, 186 рабочий стол, 386 разрешение экрана, 457 раннее связывание, 50, 96, 148, 174, 231 распределенная система, 515, 532 распределенные вычисления, 525 распределитель ресурсов, 635 регистрация сервера, 57, 66, 458 реестр, 20, 109, 401, 429 репозитарий объектов, 52 С свойства диалоговое окно, 451 инициализация, 258 получение, 478 свойства (продолжение) страницы, 99 установка, 481 файла, 451 экспонируемые, 156 элемента ActiveX, 110 свойство FormStyle, 35 Language, 462 Layout, 199 Modified, 246 TabOrder, 115 Timeout, 462 Visible, 172 связывание и внедрение объектов, 44 позднее, 70, 96, 146 раннее, 50, 96, 148, 231 связь главная-подчиненная, 539 один ко многим, 539 сеанс, 624 секция exports, 307 finalization, 589 implementation, 126, 591 initialization, 44, 56, 366, 589 interface, 128, 326 private, 116,347 protected, 110, 125 public, 343 uses, 43, 56, 114 семафор, 285 сервер OLE-документов, 235 автоматизации, 49, 131, 133, 299, 331, 464, 491 методы, 139 подготовка к созданию, 133 создание, 135 тестирование, 143 активация, 48
Алфавитный указатель 695 сервер {продолжение) баз данных, 518 без библиотеки типов, 55 внепроцессный, 43, 131, 132, 491, 629 внутрипроцессный, 43, 91, 132, 299, 331, 491 доступа к данным, 168, 492, 518, 544 приложений, 168 регистрация, 57, 66 с библиотекой типов, 58 создание, 131, 299, 332, 360, 454, 525, 607 удаленный, 43, 132 сервисы промежуточного звена, 522 сетевой протокол, 493 сетевой трафик, 635 сигнальное состояние, 272 синхронизация данных, 264 понятие, 261 потоков, 264, 288 процессов, 260, 264, 272 системный администратор, 565 системный реестр, 20, 46, 109 слайд, 194 демонстрация, 200 добавление в презентацию, 198 копирование в презентацию, 199 поиск, 199 порядковый номер, 199 цвет, 199 службы компонентов назначение, 632 принцип работы, 634 , смарт-тег интерфейс, 368 обработчик, 370 понятие, 367 распознаватель, 370 реализация, 370 событие OnChange, 162 OnClick, 113,386 OnClose, 162 OnDataChange, 246 OnDestroy, 139, 324 OnExecute, 587 OnMouseMove, 418 OnTerminate, 260 OnUpdateData, 246 события модели COM, 353 COM+, 661 публикуемые, 665 формы, 139 соглашение о вызовах safecall, 68, 335 на клиенте, 335 на сервере, 336 stdcall, 126,314,336 понятие, 302 сообщение CM GETDATALINK, 246 CM_RELEASE, 321 WM_COMMAND, 458 WM_CREATED, 457 WM DESTROY, 458 WM DROPFILES, 402 WMJCONNOTIFY, 594 WMJNITDIALOG, 457 сообщения нотификационные, 135, 159, 313, 343, 415 передача, 556 электронной почты. 206 сортйровка, 261, 550 состояние несигнальное, 272 сигнальное, 272 специальная вставка, 238 список истории, 439 стаб, 55, 75, 137, 267, 492 статический метод, 28, 489
696 Алфавитный указатель статус гостя, 630 стек крах, 302 очистка, 301 страница свойств, 99 строка состояния, 41 сценарий, 630 счетчик ссылок, 24 таблица виртуальная, 70 виртуальных методов, 28 динамических методов, 28 создание, 181 тег, 538 текст вставка в документ, 178 форматирование, 178 технология ActiveX, 91 ASP, 420,600 COM, 17 СОМ+, 638 DataSnap, 518,522,565 DCE, 51 DCOM, 493 DDE, 44,386 LRPC, 132 MIDAS, 518 OLE, 44,234 тонкий клиент, 92, 492, 522, 538 транзакция комплексная, 649 откат, 636 понятие, 635 распределенная, 636, 649 реализация, 650 сохранение результата, 636 тестирование, 656 управление, 635, 649, 660 трафик, 635 У уведомления о событиях, 661 удаленный доступ, 491 по протоколу HTTP, 511 TCP/IP, 503 с помощью сервисов DCOM, 493 удаленный сервер, 43, 132 указатель мыши, 403 универсальный клиент, 493 упакованная запись, 291 управление загрузкой модулей, 83 памятью, 22, 78 транзакциями, 649 упрощенное приложение, 566 уровень безопасности низкий, 124 средний, 126 Ф фабрика классов, 20, 45, 396, 510 файл cookie, 600, 621 временный, 243 исполняемый, 491 локальный, 541 справочный, 171 файловый поток, 542 фильтр, 41 фоновый поток, 259, 264 форма активная, 102, 535 главная, 330 дочерняя, 35, 328 модальная, 318, 615 немодальная, 323 форматирование текста, 178 функции автозавершения, 437 внутрипроцессного сервера, 83 диалоговые, 456 обратного вызова, 314, 454, 506
Алфавитный указатель 697 функции (продолжение) ожидания, 272 единственного объекта, 273 нескольких объектов, 274 синхронизации, 273 функция Alloc, 78 CoCreatelnstance, 46, 81 CoGetClassObject, 45 CoTaskMemAlloc, 80 CreateComObject, 48 CreateOleObject, 71 CreateProcess, 74 CreateSemaphore, 286 DeleteCriticalSection, 289 DidAlloc, 79 DllCanUnloadNow, 48 EnterCriticalSection, 289 FillMemory, 312 GetSize, 79 LeaveCriticalSection, 289 Realloc, 78 ShowMessage, 41 TryEnterCriticalSection, 289 X хранение информации о состоянии, 621 хранимая процедура, 579 ц цветовая схема, 196 цифровая подпись, 123 Ш шрифт, 18, 296, 457 э экспорт дочерних форм, 328 интерфейсов, 267 методов, 38 объектов, 17 функций, 53, 304 электронная почта, 206 Я ядро Windows, 19, 259 язык гипертекстовой разметки, 433 определения интерфейсов, 51, 268 программирования, 22, 462 сценариев, 121, 606 ярлык, 386
Н. Елманова, С. Трепалин, А. Тенцер Delphi и технология COM (+CD) Мастер-класс Главный редактор Заведующий редакцией Руководитель проекта Литературный редактор Художник Корректор Верстка Е. Строганова И. Корнеев В. Рычков А. Жданов Н. Биржаков В. Листова Р. Гришанов Лицензия ИД № 05784 от 07.09.01. Подписано в печать 29.08.03. Формат 70X100/16. Усл. п. л. 56,76. Тираж 3000 экз. Заказ № 515. ООО «Питер Принт». 196105, Санкт-Петербург, ул. Благодатная, д. 67в. Налоговая льгота - общероссийский классификатор продукции ОК 005-93, том 2; 953005 - литература учебная. Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. А. М. Горького Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.