This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Содержание Предисловие Выражение признательности Введение О книге Для кого написана эта книга? Начальные требования Работа с сопроводительным CD-ROM Стиль программирования Некоторые общие замечания Часть I. Вступление Глава 1 «Компоненты» Философия программирования Многократное использование кода в программировании Объектная ориентация Объектно-ориентированное программирование Многократное использование кода и объектная ориентация Многократное использование на двоичном уровне Другой пример многократного использования на двоичном уровне Создание многократно используемых объектов Нестандартные управляющие элементы Windows SDK Microsoft Visual Basic и VBX OLE! Интерфейсы и включение Automation Будущее? Создание элемента ActiveX Требования к компьютеру Создание элемента-примера Smile Что дальше? Глава 2 «ActiveX и OLE: основные положения» COM IUnknown Подсчет ссылок Другой способ определения возможностей объекта REFIID, IID, GUID и CLSID HRESULT и SCODE Мой первый интерфейсный указатель Реест IClassFactory Использование других объектов-включение Automation и IDispatch Свойства, методы и события Automation на основе IDispatch Automation на основе двойственных интерфейсов Библиотеки типов GetTypeInfoCount и GetTypeInfo Структурированное хранение Структурированное хранение и отложенная запись Структурированное хранение и элементы ActiveX Создание сложных документов средствами ActiveX Визуальное редактирование Составные документы Связанные объекты Документы ActiveX Drag-and-drop Интерфейсы Документов OLE и ActiveX
www.books-shop.com
Другие интерфейсы ActiveX IDataObject IRunningObjectTable Как больше узнать об ActiveX Глава 3 «COM-расширения для элементов» Пример работы с объектом Automation Краткое знакомство с объектом Программируемый объект как таковой Регистрация и запуск программы-примера Подробнее о библиотеках типов Возвращаемся к структурированному хранению Архитектура элементов ActiveX Языковая интеграция Свойства окружения События Точки соединения Оповещения об изменении свойств Взаимодействие элемента с контейнером Работа с клавиатурой Типы и координаты Устойчивость Наборы и комплекты свойств Биты состояния Страницы свойств Работа с отдельными свойствами Лицензирование Регистрация Обновление версий объектов Спецификация OCX 96 Активизация Внеоконные элементы Оптимизация графического вывода Прочие изменения и добавления в OCX 96 Изменения в элементах ActiveX Глава 4 «Программные инструменты Microsoft для создания элементов ActiveX» Реализация новых интерфейсов Упрощенные способы создания элементов Инструменты для создания элементов на C++ Создание элементов при помощи MFC Так что же сделал мастер? Класс модуля элемента: CFirstApp Класс элемента: CFirstCtrl Класс страницы свойств: CFirstPropPage Спецификации OCX 96 и ActiveX при создании элементов с использованием MFC Runtime-библиотеки MFC Построение и тестирование элемента First в тестовом контейнере Работа с тестовым контейнером Создание элементов при помощи ActiveX Template Library (ATL) Создание элементов при помощи шаблона ActiveX BaseCtl Создание элементов ActiveX на языке Java в среде Visual J++ Примечания по поводу примеров, использованных в этой книге Часть II. Основы элементов ActiveX Глава 5 «Свойства» Стандартные свойства окружения Некоторые расширенные свойства Свойства элементов Добавление стандартных свойств Новые свойства начинают работать Программный доступ к свойствам элемента Добавление нестандартных свойств
www.books-shop.com
Построение и тестирование элемента Свойства элементов в других библиотеках Глава 6 «Устойчивость свойств: сериализация» Подготовка Устойчивость свойств (с использованием MFC) Другие PX-функции Устойчивость стандартных свойств Устойчивость свойств (без использования MFC) Глава 7 «Методы» Элементы ActiveX и нестандартные методы Добавление нестандартного метода в элемент на базе MFC Простейшая база данных для HRESULT Структура базы данных HRESULT Ошибки и исключения Добавление методов в элементы, написанные без использования MFC Глава 8 «События» Возможные применения событий Типы событий Request-события Before-события After-события Do-события Инициирование событий Стандартные события События, MFC и Visual C++ Добавление стандартного события Добавление нестандартного события Добавление нестандартных событий в элемент First Реализация событий без MFC Глава 9 «Ошибки и исключения» Что такое «исключение»? Обработка исключений в MFC и C++ Обработка исключений в элементах ActiveX Исключения и двойственные интерфейсы Обработка исключений элементом First Обработка исключений без использования MFС Глава 10 «Консолидация» Проектирование элементов Визуальные и составные элементы Объектная модель элемента Субклассирование элементов Раскрывающиеся списки со значениями свойств Работа с базами данных в элементах ActiveX Сброс состояния элемента Отладка элемента Версии элемента Справочные файлы для элементов Глава 11 «Страницы свойств» Что такое страницы свойств? Как работать со страницами свойств Проектирование страниц свойств Отображение свойств, доступных только для чтения Дополнительные страницы свойств Стандартные страницы свойств
www.books-shop.com
Использование справки в страницах свойств Страницы свойств без MFC Интерфейсы, раскрываемые объектами страниц свойств Глава 12 «Классы ColeControl и ColePropertyPage» ColeControl Automation — свойства, методы и события Обработка ошибок и исключения Automation Функции, обеспечивающие устойчивость свойств Функции, относящиеся к ActiveX OCX 96 и расширения ActiveX в классе ColeControl ColePropertyPage Часть III. Элементы ActiveX для профессионалов Глава 13 «Элементы ActiveX и Internet» Применение элементов ActiveX в Web-страницах Внедрение элементов в Web-страницы Указание начального состояния элемента Путевые свойства Взаимодействие с элементами на Web-странице Специфика элементов, предназначенных для работы в Web ActiveX Control Pad и HTML Layout Control Глава 14 «Нестандартные шрифтовые и графические свойства» Элемент Children Использование стандартного шрифтового свойства Реализация нового интерфейса для обмена информацией со шрифтовым объектом Функция проверки Трудности с рисованием Глава 15 «Связывание данных» Механизм связывания данных в элементах ActiveX Создание элемента со связанным свойством Проверка элемента в тестовом контейнере Оповещение об изменении свойства Прочее Глава 16 «Лицензирование» Проблема лицензирования Основные концепции лицензирования элементов ActiveX Лицензирование в MFC Создание лицензионного элемента Модификация лицензионной схемы Многоуровневое лицензирование Модификация элемента License для многоуровневого лицензирования Лицензирование элементов в Web-страницах Формат LPK-файла Создание LPK-файлов Глава 17 «Интерфейс ISimpleFrameSite» Интерфейс ISimpleFrameSite Глава 18 «Конвертирование VBX и субклассирование элементов Windows» Преобразование VBX Что же делает OLE ControlWizard Конвертирование VBX Структура, определяемая программистом Обработка сообщений Обработка VBM-сообщений Растры панели элементов
www.books-shop.com
Некоторые проблемы, относящиеся к свойствам и методам Функции Visual Basic API и новые интерфейсы Некоторые ошибки и ограничения Субклассирование элементов Windows Возвращаемся к элементу Children Глава 19 «16/32-разрядные операционные системы и кросс-платформенные проблемы» Кросс-платформенные проблемы Проблемы перехода от 32- к 16-разрядной версии Выравнивание Unicode, ANSI и MCBS Естественные отличия Сообщения и изменения в API Отличия в Windows Отличия в инструментарии Потоки Взаимодействие COM-объектов с разной разрядностью Глава 20 «Рекомендации для элементов ActiveX и контейнеров» Интересные возможности Отражение сообщений Автоматическое отсечение Перегрузка IPropertyNotifySink Специфические интерфейсы контейнеров Общие рекомендации Протокол обработки событий Многопоточность в элементах Часть IV.Приложения Приложение А « Visual C++, MFC и ATL: создание COM-объектов» Библиотека MFC Переносимость MFC Эволюция MFC Структура MFC Приемники команд и схемы сообщений Класс приложения CWinApp CWnd и производные от него классы Механизмы вывода Документы и виды Шаблоны документов Документы Виды Другие классы Служебные классы и исключения Элементы и диалоговые окна Глобальные функции и макросы COM, ActiveX и поддержка OLE Схемы диспетчеризации Документы ActiveX Создание других COM-интерфейсов — схемы интерфейсов ODBC Инструменты Visual C++ для работы с MFC AppWizard ClassWizard Редактирование ресурсов AutoPro3 ActiveX Template Library (ATL) Приложение Б « Потоковые модели COM» Общие сведения Дополнительная информация
www.books-shop.com
Совместная модель Свободная модель Смешанная модель Выбор потоковой модели Пометка поддерживаемой потоковой модели Когда клиент и объект пользуются различными потоковыми моделями Потоковые модели во внутрипроцессных серверах Взаимодействие клиента и внутрипроцессного объекта с различными потоковыми моделями Потоковые модели во внепроцессных серверах Потоковые модели в клиентах
www.books-shop.com
Предисловие Панацея: 1. 2.
Универсальное лекарство от всех болезней, средство для решения любых проблем. Internet, World Wide Web и любые связанные с ними (даже очень отдаленно) технологии.
Я всегда рассматривал программистов с несколько циничной точки зрения — по-моему, они больше похожи на хитрых художников-халтурщиков, нежели на ученых-исследователей. Многие тысячи людей, которые целыми днями просиживают за компьютерами и занимаются различными аспектами «панацеи Internet», втайне полагают, что окружающий мир сошел с ума и благодаря этому можно всегда заработать на кусок хлеба с маслом. Пожалуй, закоренелый циник определил бы Internet как средство имитировать полезную деятельность и одурачивать обладателей модемов красивыми картинками, благодаря которому можно вернуться к уровню 1984 года и заставить современный процессор P6-200 работать, как архаичный 8088 с тактовой частотой 4 Мгц. Недавно мой скепсис получил очередное подтверждение — в прессе мне попались две статьи. В первой сообщалось, что развитие Internet «предвещает гибель Microsoft», тогда как во второй Internet был назван «спасением Microsoft». Между публикациями обеих статей не прошло и месяца! Где-то между вдохновенной болтовней «провидцев» и брюзжанием мизантропов отыскалась ниша для довольно любопытного семейства технологий. ActiveX представляет собой второе поколение сервиса, основанного на модели компонентных объектов (COM). Хочется верить, что эта технология хотятакие «расширения» даже при небольшом объеме могут оказывать довольно заметное влияние на работу пользователя. На самом деле Web требует, чтобы максимум возможностей был втиснут в минимум объема. Желая облегчить «наступление нового порядка», Адам Деннинг, я и многие другие стали добиваться такой переработки элементов OLE, чтобы для установки их ActiveX-версий в контейнерах не требовалось никакой особой функциональности. Мы решили, что элементы должны делать только то, что действительно необходимо, а контейнер не должен требовать от них поддержки функций, не имеющих ничего общего с основной задачей. Бар: 1.
2.
Заведение, где продаются и потребляются алкогольные напитки. Без комментариев.
Мы с Адамом Деннингом — старые друзья. Думаю, у него получилась замечательная книга. Его предыдущая книга «OLE Controls Inside Out» (Microsoft Press, 1995) была посвящена созданию элементов OLE на базе MFC, а его текущее участие в работе над библиотекой шаблонов ActiveX Template Library (ATL), предназначенной для разработки элементов ActiveX, делает его исключительно квалифицированным специалистом по данной теме и настоящим авторитетом в области ActiveX. Но больше всего мне нравится в книге Адама то, как он преподносит материал читателю. Его опыт реального программирования и умение рассказать именно то, что нужно программисту, становятся самым важным достоинством книги. Знакомство с темой пригодится не только программистам — особый интерес представляет рассказ об истории развития элементов ActiveX наряду с феноменальной глубиной технических познаний автора. Эта книга — превосходный подарок, поскольку менеджер найдет в ней подборку уникальных сведений о технологии ActiveX, а для программиста она станет лучшим справочником по данной теме. В общем, книга совершенно необходима в любом хозяйстве. И менеджеры и программисты первым делом должны осознать, что элементы ActiveX не привязаны ни к какому конкретному языку программирования или программной библиотеке. Фактически вы можете писать их на C или C++ в любой рабочей среде — или вообще без рабочей среды! В этом и заключается один из секретов того, как нам удалось за кратчайшее время внедрить полноценную поддержку ActiveX в Microsoft Internet
www.books-shop.com
Explorer — нам не пришлось заменять существующие HTML-документы, программные библиотеки или сервис, достаточно было сделать ActiveX похожими на COM. Очередная панацея? Мне вспоминается старый рекламный ролик, в котором комик жаловался на то, что кафе быстрого обслуживания не приносит ему жизненного счастья — может быть, там можно найти дешевые гамбургеры, но никак не счастье. Ни одно отдельное технологическое новшество не способно справиться со всеми проблемами, оно может лишь внести свой вклад в решение целого класса задач, на который направлены ваши усилия. Но даже если отбросить рекламную шелуху, следующие утверждения окажутся истинными:
COM предоставляет неплохую возможность для того, чтобы организовать общение и обмен данными между разнородными технологиями. ActiveX — специализированный набор средств, построенных на базе COMтехнологий и предназначенных для решения задач, связанных с Internet и интрасетями. В условиях работы с HTML, сценариями и расширяющими компонентами системы элементы ActiveX позволяют легко и безопасно наращивать возможности платформы независимо от языка программирования, программных библиотек и самой платформы. Данная книга лучше других научит читателя создавать элементы ActiveX и взаимодействовать с ними. На самом деле вам вообще не придется читать какуюлибо другую литературу по данной теме, поскольку все нужное и полезное изложено на страницах этой книги. Напоследок передаю привет своему другу Адаму — не могу устоять перед искушением. Счастливого программирования и до встречи в Сети! Виктор Стоун, менеджер по разработкам, Microsoft Corporation
www.books-shop.com
Выражение признательности Основная часть людей, помогавших мне с первым изданием книги, также помогла и в работе над вторым. Конечно, кое-кто отказался и даже заявил, что незнаком со мной. Другие настолько увлеклись поисками всех недостатков первого издания, что их следует поблагодарить хотя бы за здоровую критику во время переработки книги. Наконец, третьи пришли мне на помощь совсем недавно, за что я им также искренне благодарен. Многие из них помогали мне вполне сознательно, другие делились полезной информацией и облегчали мою работу, не подозревая об этом. Ряд исключительно полезных замечаний был получен от читателей в Microsoft и за ее пределами, которые предложили новые темы, обратили мое внимание на ошибки и недочеты предыдущего издания и задали вопросы, над которыми мне пришлось изрядно поломать голову. Большая часть этих замечаний была учтена во втором издании. И снова выражаю свою любовь и почтение своей жене Мелиссе и нашим трем дочерям — Асфодель, Фиби и Ксанф. И снова Марк Деймонд (Mark Daymond), мой друг из английского представительства Microsoft (который остался моим другом в Редмонде, несмотря на его комментарии по поводу первого издания), так и не внес сколько-нибудь заметного вклада в работу. Впрочем, наш общий друг, Билл Чемпион (Bill Champion), снизошел до написания предисловия к этой книге — я благодарен ему, невзирая на все, что в нем написано. [К сожалению, предисловие г-на Чемпиона в этой книге отсутствует.] Разумеется, в создании книги мне помогали многие работники Microsoft. Особенно мне хотелось бы поблагодарить редактора Лайзу Теобальд (Lisa Theobald). Даже несмотря на то, что в этом проекте состоялся ее дебют в должности редактора, ей как-то удалось сохранить здравый рассудок при всем моем умении опаздывать со сдачей материала (и даже опаздывать после новой назначенной даты — впрочем, здесь это стало нормой). Благодарю и неподражаемого Марка Янга (Mark Young), который снова «управился» с техническим редактированием текста, и притом сделал это превосходно. Кстати говоря, если по первому изданию вы сочли меня неисправимым пессимистом, учтите — идея связать все иллюстрации с персонажами и цитатами из телесериала «Красный карлик» принадлежала именно Марку. Эрик Стру (Eric Stroo), выпускающий редактор Microsoft Press, выпускал и редактировал выше всяких похвал. Назову лишь некоторых специалистов из Microsoft, помогавших мне в работе над книгой, — Дейв Масси (Dave Massy), Виктор Стоун (Victor Stone), Джим Спрингфилд (Jim Springfield), Кристиан Бомонт (Christian Beaumont), Джен Фалкин (Jan Falkin), Джон Элсбри (John Elsbree), Ян ЭллисонТейлор (Ian Ellison-Taylor), Майк Блашак (Mike Blaszack), Дин МакКрори (Dean McCrory), Нат Браун (nat Brown), Чарли Киндел (Charlie Kindel) и Гэри Берд (Gary Burd). Эрик Ланг (Eric Lang) и Денис Гилберт (Denis Gilbert) по сути дела разрешили мне написать эту книгу и прикрывали глаза в те моменты, когда я должен был заниматься своей основной работой. Моя признательность не знает границ. Кроме того, Виктор в рекордно короткое время написал замечательное предисловие [которое присутствует в этой книге], за что я ему искренне благодарен. Наконец, хочу поблагодарить Хана Басби (Khan Busby)(да, его действительно именно так зовут!) за то, что он когда-то приобщил меня к компьютерам. С другой стороны, моя жена Мелисса хочет его убить по той же самой причине. Весьма возможно, что я забыл кого-нибудь упомянуть и тем самым смертельно обидел. Если вы уверены в том, что помогали мне и не нашли своего имени в этом разделе, свяжитесь со мной по электронной почте — я непременно перешлю ваше сообщение в службу поддержки.
Введение Я перестал радоваться техническому прогрессу. В конце концов, если бы все вокруг оставалось без изменений, то мне не пришлось бы тратить лучшие годы жизни на обновление этой книги. Однако жизнь не стоит на месте, и то, что раньше занимало мелкую технологическую нишу, неожиданно приобрело значительно большее значение. Я решил, что мне обязательно следует пересмотреть эту книгу и включить в нее побольше технологических новинок, насколько мне позволит весьма напряженный график работы. Как минимум в двух отношениях моя задача оказалась невыполненной — к моменту завершения рукописи два программных инструмента, о которых я собирался писать, еще не работали достаточно надежно. Речь идет о библиотеке шаблонов ActiveX Template Library (ATL) версии 2.0 и программных средствах, которые бы позволяли писать настоящие элементы на Java. К тому моменту, когда вы будете читать эту книгу, библиотека ATL 2.0 наверняка выйдет в свет. Выходит, что книга писалась слишком медленно, но по иронии судьбы издание вышло слишком рано! Согласитесь, подобное встречается нечасто. Во время работы над первым изданием этой книги я работал старшим консультантом в службе Microsoft Consulting Service (MCS) в Англии. К моменту завершения второго издания я перешел на должность менеджера проекта в отделе визуальных языков Microsoft в Редмонде и руковожу разработкой библиотек для Visual C++. С момента выхода первого издания количество моих дочерей не изменилось — насколько мне известно, последней является Дочь версии 3.0.
О книге Практически любой программист для Windows, какими бы программными средствами он ни пользовался, знает о существовании нестандартных управляющих элементов Microsoft Visual Basic, или так называемых VBX. Скорее всего он также слышал об элементах OLE и об их широко разрекламированном превращении в элементы ActiveX. VBX представляют собой внешние дополнения к Visual Basic, которые определенным образом расширяют круг его возможностей (эта тема подробно рассмотрена в главе 1). Когда Visual Basic впервые появился на рынке в 1991 году, никаких VBX в нем не было. Сейчас существуют целые сотни VBX (и элементов ActiveX), распространяемых на коммерческой основе, и многие тысячи их были разработаны для внутреннего использования в компаниях всего мира. Они уже не ограничиваются средой Visual Basic, хотя (большей частью) такие элементы все же привязаны к 16-разрядным платформам. Microsoft Visual C++ стал одной из первых программных сред, в которых была внедрена поддержка VBX. Его примеру последовали многие языки программирования и средства разработки. Рынок растет, на нем появляется все больше VBX, рынок снова расширяется и т. д. Впрочем, при этом возникает одна проблема. Архитектура, лежащая в основе VBX, не переносится на 32-разрядные или не-Intel платформы. Что же делать? Если вы занимаетесь разработкой коммерческих VBX и следите за нарастанием популярности 32-разрядных операционных систем, такой вопрос оказывается особенно важным — от него зависит ваше существование. Даже если ваши разработки предназначены для внутреннего использования, проблема все равно остается, поскольку в какой-то момент ваша компания неизбежно перейдет на 32-разрядную операционную систему и захочет переделать под нее все рабочие приложения. Вы окажетесь в безвыходном положении — можете немедленно увольняться и переходить на торговлю подержанными автомобилями. Но в таком случае вы бы не держали в руках эту книгу, а это совершенно немыслимо, поскольку мои дети начали бы узнавать своего папу в лицо. Значит, выход все же имеется. Элементы ActiveX способны выполнять те же задачи, что и традиционные VBX, но при этом они могут работать на разных платформах — как на 16-, так и 32-разрядных (по крайней мере, в определенной степени). Кроме того, они обладают более широкими возможностями. Вся эта книга посвящена разработке элементов ActiveX. Сначала их работа рассматривается с концептуальной точки зрения. Разумеется, для этого нужно хотя бы в общих чертах представлять себе работу COM, так что в нескольких начальных главах мы изучим базовые принципы работы COM, ActiveX и OLE (если вам захочется узнать о них побольше, прочитайте превосходную книгу Крейга Брокшмидта (Kraig Brockschmidt) «Inside OLE», второе издание (Microsoft Press, 1996). В главе 1 обсуждается концепция компонентов — новой категории программ, которые представляют собой блоки многократно используемого кода, удобно упакованные и приспособленные для работы во многих различных средах. Кроме того, в ней содержится первый
www.books-shop.com
работающий (хотя и совершенно бесполезный!) элемент ActiveX в этой книге. Глава 2 начинается с рассмотрения COM — что это такое, для чего нужно и как работает. В главе 3 приведено более глубокое описание COM, а также обсуждается специфика моделей COM и ActiveX по отношению к элементам ActiveX. Если вы уже знаете об ActiveX все необходимое или же хотите просто создавать свои элементы ActiveX, не останавливаясь на принципах их работы, можете пропустить главы 2 и 3. В главе 4 мы перейдем к практической работе — в ней представлены программные инструменты Microsoft для создания элементов ActiveX. Большая часть изложенного материала посвящена Visual C++ и библиотеке Microsoft Foundation Classes (MFC), однако мы кратко рассмотрим и другие средства. В нескольких последующих главах рассматриваются атрибуты самих элементов, начиная со свойств, о которых речь идет в главе 5. В главе 6 кратко обсуждается устойчивость свойств, или способность контейнера (приложения, в котором используются элементы) при содействии самого элемента хранить информацию о его состоянии между сеансами. В главе 7 рассказано о методах элементов — одной из областей, в которых элементы ActiveX явно превосходят по своим возможностям VBX. Глава 8 посвящена событиям — так называются сообщения, которые контейнер получает от своих элементов. В главе 9 рассматриваются мощные средства обработки ошибок и исключений в элементах. Глава 10 объединяет все эти темы и добавляет к ним несколько новых. Продвигаясь от главы 5 к главе 10, мы будем создавать и модифицировать элемент ActiveX, который примет свой окончательный вид в главе 10. Глава 11 рассказывает о новой возможности, присущей элементам ActiveX, — страницах свойств (property pages). В главе 12 рассматриваются классы MFC, предназначенные для создания элементов и страниц свойств, а темой главы 13 является взаимодействие элементов с Internet и World Wide Web. Глава 14 показывает, как в элементах используются шрифтовые свойства. В главе 15 кратко описан процесс связывания данных, при котором свойства элемента ассоциируются с полями базы данных (как мы убедимся, эта схема является открытой, так что контейнер может соединить свойство с любым объектом). В главе 16 показано, как можно регулировать использование вашего элемента при помощи лицензирующих дополнений COM, а глава 17 посвящена элементам, которые (визуально) содержатся внутри других элементов, — например, кнопки-переключатели в стандартной групповой панели Windows. В главе 18 изучаются две темы, которые часто рассматриваются вместе, — как преобразовать VBX в элемент ActiveX и как создать элемент ActiveX, который субклассирует стандартный управляющий элемент Windows. Глава 19 посвящена аспектам создания элементов для 16- и 32-разрядных платформ, а также проблемам переносимости. Наконец, в главе 20 рассматривается взаимодействие элемента с контейнером и даются некоторые рекомендации, соблюдение которых гарантирует совместимость элементов и контейнеров, созданных с их учетом. Книга завершается двумя приложениями. В приложении А содержится краткое описание компилятора Microsoft Visual C++ и прилагаемой к нему библиотеки MFC. Оно предназначено для тех читателей, которые работают с другими компиляторами или вообще не пользуются библиотеками классов. Приложение Б представляет собой переработанную статью из KnowledgeBase и описывает некоторые тонкости работы программных потоков (thread) в COM.
Для кого написана эта книга? Разработка элементов ActiveX может быть несложным делом; если вы хорошо знакомы с технологией COM, то и понять их будет легко. Эта книга предназначена для людей, которые хотят больше узнать об элементах ActiveX и о том, как их создавать.<> Ее главная задача — подвести вас к пониманию того, как работают элементы ActiveX и как писать их на C++. Поскольку эта книга в первую очередь является учебником, основное внимание в ней сосредоточено на создании элементов ActiveX с использованием MFC, тем не менее в ряде мест мы обсудим и другие средства их разработки на C++. Наибольший интерес из таких средств представляет библиотека Microsoft ActiveX Template Library (ATL). В версию ATL 2.0, которая появится уже после того, как моя книга отправится в Microsoft Press, будут включены средства для ускоренного создания малых элементов ActiveX, однако для того, чтобы пользоваться ими, необходимо сначала в совершенстве освоить COM и C++. Кроме того, мы кратко обсудим возможности создания элементов на Java. В момент написания книги соответствующие средства также не были готовы, так что мы не сможем рассмотреть их во всех подробностях. Мы увидим, как элементы используются в крупных приложениях-контейнерах (особое внимание уделено Microsoft Internet Explorer и Visual Basic) и какие проблемы возникают при переносе
www.books-shop.com
элементов ActiveX на другие платформы. Опыт работы с MFC или Visual C++ необязателен, но желателен. Все примеры элементов ActiveX в этой книге написаны с использованием MFC и Visual C++, так что построить их можно только при наличии этих программных продуктов. Для построения примеров не требуется никаких особых познаний в области COM или ActiveX — как упоминалось выше, весь необходимый материал будет предварительно изложен. Я глубоко почитаю концепцию программ-компонентов, воплощенную в элементах ActiveX, поэтому я постарался не только по возможности упростить их создание на C++, но и продемонстрировать их истинную ценность для программистов самого широкого круга — от разработчиков, которые трудятся в крупных компаниях над проектами для внутреннего использования, до фирм, занимающихся производством коммерческих пакетов. С этой же целью я приложил все усилия к тому, чтобы примеры в этой книге были полезными и могли использоваться на практике. Я буду считать свою задачу выполненной, если после прочтения книги начинающий разработчик элементов ActiveX почувствует себя достаточно уверенно для самостоятельной работы, что, в свою очередь, будет способствовать здоровой конкуренции на рынке компонентов.
Начальные требования Чтобы пользоваться примерами из этой книги и создавать свои собственные элементы ActiveX, необходимо обладать определенными начальными навыками. Вы должны уметь программировать на C++ для Microsoft Windows на уровне SDK или с использованием библиотеки MFC версии 3.0 и выше (библиотека MFC 3.0 входила в комплект Visual C++ 2.0). Поскольку во многих примерах этой книги используется MFC (а сама книга проектировалась как учебник), опыт работы с этой библиотекой окажется полезным для читателя. Предварительный опыт программирования для COM или ActiveX не является обязательным, но любые знания в этой области не будут лишними. Вам также понадобятся некоторые программные средства. Элементы, рассматриваемые в книге, компилируются любым 32-разрядным компилятором C++, работающим с MFC 4.0 и более поздних версий. Все примеры были протестированы на Visual C++ 4.2 в операционных системах Microsoft Windows 95 и Microsoft Windows NT 4.0 (на компьютере с процессором Intel Pentium). При тестировании использовалось приложение Test Container, входящее в состав Visual C++, а также Internet Explorer 3.0 и 4.0, Visual Basic 4.0 и Visual C++ 4.2. Все программирование (вместе с набором текста книги) происходило в системе Windows 95 на нескольких портативных компьютерах Toshiba, из которых самым достойным был Tecra 720CDT с 48 Мб памяти — обожаю! Текст книги в основном набирался в Microsoft Word для Windows 95, хотя в ряде последних фрагментов использовалась бета-версия Word 97. При выборе компьютера одним из самых главных факторов, влияющих на работу компилятора C++ и вспомогательного инструментария, является объем оперативной памяти — чем больше, тем лучше. Да, конечно, процессор и объем жесткого диска тоже следует учитывать, и все же самый быстрый процессор при нехватке памяти ведет себя крайне скверно. Я бы порекомендовал как минимум 32 Мб для Visual C++ 4.x под Windows 95 и Windows NT.
Работа с сопроводительным CD-ROM Прилагаемый к книге CD-ROM содержит исходные тексты и make-файлы для построения всех примеров программ и элементов ActiveX в среде Visual С++ 4.2. На диске нет ни одного откомпилированного элемента. Вы можете самостоятельно откомпилировать любой проект, который вас заинтересует. Кроме того, на диске имеются и другие файлы — например, ActiveX SDK и Internet Explorer 3.0. Чтобы перенести файлы с CD-ROM на жесткий диск, вставьте компакт-диск в дисковод CD-ROM вашего компьютера. В операционных системах Windows 95 и Windows NT при этом обычно автоматически запускается программа Setup. Если этого не происходит, вероятно, ваш дисковод CD-ROM не сообщает операционной системе о смене диска (или данная возможность отключена). В таком случае запустите программу SETUP.EXE из корневого каталога CD-ROM. После запуска программы Setup выполняйте инструкции на экране.
Стиль программирования
www.books-shop.com
У меня есть собственный стиль программирования, которым я пользуюсь в этой книге. Кое-кто относится к подобным вещам чрезвычайно болезненно, так что при желании можете переделать тексты программ в свой любимый формат. Обычно я стараюсь пользоваться скобками даже в тех случаях, где средства языка позволяют обойтись без них — на мой взгляд, это позволяет сократить количество ошибок в программе. Кроме того, отступы фигурных скобок я расставляю в «вертикальном» стиле (также известном как «Единственно верный», все остальные — признак глубокого невежества): void CmyClass::MemberFunction(LPCSTR lpszTitle, short nLength)
{
}
if (nLength == 0) { return; } for (int i = 0; i < nLength; i++) { if (lpszTitle[i] == ‘A’) { MsgBox("This string contains an ‘A’!"); break; } }
Стилем программирования, использованным в первом издании книги, я нажил себе множество врагов. В интересах своей личной популярности(а также потому, что старый стиль теперь мне кажется уродливым — только не подумайте, что я так легко поступаюсь принципами) я слегка изменил его. Если в прошлом я обожал пустые места, то теперь мои наклонности стали более умеренными, как можно убедиться по приведенному выше фрагменту. В именах переменных я в общем и целом стараюсь придерживаться «венгерской записи», используемой в Microsoft. Тем не менее мой «венгерский диалект» отличается от общепринятого. Кроме того, я пользуюсь дополнительными соглашениями из MFC и ATL — например, буква C в начале имени класса говорит о том, что это имя класса, а не что-то другое (некоторые программисты терпеть этого не могут).
Некоторые общие замечания Когда речь заходит о компиляторе Visual C++, я указываю либо конкретную версию (скажем, 4.2), либо целое семейство (скажем, 4.x). И то и другое делается совершенно намеренно. Аналогично, понятие Windows 3.x относится к 16-разрядным версиям Windows (таким, как 3.1 или Windows for Workgroups). Win32, если только в тексте прямо не сказано противоположное, означает Windows 95 со всеми дополнительными пакетами и OEM-версиями, а также Windows NT версии 3.51 и выше. Тем не менее Windows NT 4.0 относится к версиям Windows NT выше 3.51. Подсистема Win32s упоминается очень редко, поскольку она почти не используется, а уровень поддержки COM в ней не позволяет создавать нормальные элементы ActiveX. Пожалуй, самым странным в этой книге для меня стали те изменения, которые мой природный юмор и безукоризненная грамматика вытерпели при переводе книги на американский диалект. Я был буквально ошарашен и уничтожен, когда прочитал, что я «выставил» (а не установил) бетаверсию операционной системы на свой компьютер. Прошу учесть, что все несмешные шутки обусловлены исключительно редакторской правкой или нехваткой чувства юмора у читателя. В этой книге я не смог подробно остановиться на двух темах: эффективность работы элементов ActiveX и вопрос о том, когда следует пользоваться элементами, а когда стоит поискать другое решение. Впрочем, у меня не было особого желания об этом писать. Насчет эффективности: элементы, использующие COM, должны быть жирными и неповоротливыми, не так ли? Более того, для их работы понадобится множество DLL-библиотек, которые еще нужно где-то взять? Ничего подобного. Прежде всего, в большинстве управляющих элементов Windows установка атрибутов осуществляется при помощи механизма сообщений Windows, а в элементах ActiveX для этого применяется Automation. При вызове функций Automation в большинстве случаев расходуется значительно меньше команд процессора, чем при традиционных вызовах
www.books-shop.com
PostMessage или SendMessage, особенно если при этом используется двойственный интерфейс. Возможна экономия времени примерно в 10 раз. С другой стороны, установка элемента на экранной форме и его перерисовка для элементов ActiveX обходится значительно дороже (если только в ваших элементах не используются внеоконные средства OCX 96). Накладные расходы, связанные с общением между элементом и контейнером, а также активацией элементов, приводят к тому, что элементы ActiveX в большинстве случаев создаются медленнее, чем традиционные управляющие элементы Windows. Это и стало одной из главных причин для внесения изменений OCX 96 в исходную спецификацию элементов OLE (см. главы 2 и 3). Наконец, можно принять концепцию компонентов слишком близко к сердцу и пользоваться элементами ActiveX в любых ситуациях, словно это библиотечные функции или системный сервис. Microsoft почти поощряет такое отношение — все большая часть системных функций становится доступной через элементы ActiveX. Тем не менее это не всегда разумно. Существует ли правило, которое определяет, стоит вам пользоваться элементами ActiveX или нет? Вероятно, не существует, особенно если учесть новые рекомендации из главы 20, по которым элементом ActiveX можно считать едва ли не все, что поддерживает интерфейс IUnknown. И все же решение можно принять, руководствуясь обычным здравым смыслом.
www.books-shop.com
Глава
1
Компоненты Эта глава закладывает основу для всей книги. В ней мы рассмотрим некоторые концепции и проблемы, которые заставили программистов медленно, но неотвратимо переходить на объектноориентированные технологии. Затем мы убедимся, что во многих отношениях преимущества объектно-ориентированного подхода были реализованы совсем не так, как ожидалось, и все же одно из направлений его развития выглядит очень перспективным. Речь идет о создании объектов, которые можно было бы использовать многократно в окончательной, исполняемой форме, а не на уровне исходных текстов или компоновки. Такие объекты похожи на адаптеры персональных компьютеров или автомобильные стереосистемы: они выполнены по одним и тем же стандартам, и потому их можно свободно заменять аналогичными объектами. На этом принципе основана работа так называемых «программных компонентов» (componentware). Первым настоящим проявлением этого феномена стали нестандартные управляющие элементы Microsoft Visual Basic, или VBX. Впрочем, VBX далеко не идеальны, и спецификация элементов ActiveX призвана исправить их основные недостатки — плохую переносимость и слишком тесную связь с Visual Basic. Ту же задачу решает и среда Java, которая, как мы вскоре убедимся, имеет непосредственное отношение к элементам ActiveX (которые раньше назывались элементами OLE). Ах да, совсем забыл — в этой главе также описан процесс создания очень простого элемента ActiveX на C++ при помощи лишь одного из многочисленных средств разработки управляющих элементов, библиотеки Microsoft Foundation Classes (MFC).
1.1 Философия программирования Процесс создания программных продуктов почти не изменился за прошедшие годы: люди пишут программы на хитроумных языках, в которых может разобраться лишь посвященный. Программисты и даже менеджеры проектов всегда недооценивают время и ресурсы, необходимые для завершения того или иного проекта. При этом поражает одно странное обстоятельство. Тысячи программистов работают в течение многих лет, но объем кода, используемого сразу в нескольких проектах, оказывается чрезвычайно малым даже в пределах одной компании. Недавно в этом можно было упрекнуть даже Microsoft, поскольку в отдельных продуктах Office (таких, как Microsoft Word и Microsoft Excel) для одних и тех же стандартных средств (например, панелей инструментов и строк состояния) применялся разный код!
1.2 Многократное использование кода в программировании Необходимость заново изобретать программы и алгоритмы приводит к невероятным затратам ресурсов. В итоге нам приходится ломать голову над давно решенными задачами. Давайте зададимся вопросом: а удалось ли кому-нибудь реально добиться многократного использования программного кода? На первых порах программисты пытались обмениваться исходными текстами, то есть брать тексты нужных функций у коллег и друзей. Тем не менее со временем в эти копии вносились различные изменения, так что ни о каком многократном использовании говорить не приходилось. Библиотеки функций (наподобие тех, что входят в комплект вашего любимого компилятора C++) делятся на две категории: «стандартные» библиотеки, присутствие которых является обязательным (например, классические функции stdio в языке C), и «нестандартные» (скажем, прилагаемые к компилятору библиотеки для работы с графикой). Отличия между ними заключаются в том, что по крайней мере теоретически все реализации стандартных библиотечных функций (например, printf) должны получать одинаковые параметры, выполнять одинаковые действия и возвращать одинаковые результаты. Функции нестандартных библиотек таким правилам не подчиняются и почти наверняка ведут себя по-разному.
www.books-shop.com
Тем не менее никто не станет спорить с тем, что библиотеки содержат набор многократно используемых программ. Проблемы начинаются в тот момент, когда нам захочется изменить работу той или иной функции. Вернемся к примеру с printf. Если потребуется, чтобы эта функция выполняла какие-то дополнительные задачи, придется либо переписывать ее заново, либо платить фирме-разработчику компилятора за исходный текст и вносить в него необходимые изменения. После перекомпиляции исходного текста у нас появляется новая функция. Но что произойдет, если оставить ей старое имя printf? Возможны любые беды. Более того, желанное многократное использование снова исчезает, поскольку наша нестандартная функция не входит в состав компилятора. По иронии судьбы, из всех профессий именно программисты в наибольшей степени страдают синдромом «сделай сам», так что совместное использование кода в большинстве случаев происходит только на словах, а не на деле.
1.3 Объектная ориентация «Объектная ориентация» (или в общепринятом сокращении ОО) должна была раз и навсегда решить проблему многократного использования. Лично я отношусь к числу ее приверженцев, но должен признать, что до полного воплощения всех надежд еще далеко. Концепция ОО представляет собой набор идей, каждая из которых оказывается полезной для программистов (и для пользователей, как мы вскоре убедимся). Что-нибудь подобное было бы нелишним и в наши дни. Разумеется, ОО граничит с религией, и на эту тему существует множество точек зрения. Приведенное ниже описание следует считать скорее моим личным мнением, нежели бесспорным фактом (к счастью, в своем мнении я не одинок). ОО базируется на трех основных принципах:
• • •
Инкапсуляция — возможность скрыть детали реализации объекта от программиста. Наследование — возможность создавать новые объекты на базе существующих (то есть по сути — многократное использование!). Полиморфизм — проявление различных вариантов поведения (подробности см. ниже).,
Полиморфизм довольно часто является одним из проявлений наследования, хотя это и необязательно. Представьте себе иерархию, во главе которой стоит «насекомое», от которого порождено несколько конкретных разновидностей насекомых: пчелы, осы, мухи и муравьи (рис. 1-1). Насекомые кусают людей — назовем эту способность методом HurtHuman (под «методом» будем понимать некоторое действие, выполняемое объектом). Все разновидности насекомых автоматически наследуют метод HurtHuman. Тем не менее для каждой разновидности определяется собственный метод HurtHuman, который делает что-то специфическое для данного объекта — скажем, оса просто жалит, а пчела жалит и затем умирает. Каждый из четырех производных методов переопределяет поведение, заданное в методе объекта «насекомое».
Рис. 1-1. Иерархическое дерево насекомых Термин «переопределяет» означает, что вместо методов базовых объектов используется метод производного объекта. Теперь представим себе, что у нас есть некоторый процесс (необязательно в программном смысле), который работает с объектами-насекомыми. Необходимо, чтобы он умел работать с любыми насекомыми, а не только с теми, о которых нам известно на данный момент. Известно, что любой тип насекомых обладает методом HurtHuman (или пользуется методом HurtHuman базового объекта «насекомое», если этот метод делает именно то, что нужно для данного типа).
www.books-shop.com
Наш процесс умеет работать с базовым типом насекомых, но его поведение должно оставаться по возможности общим — он не обязан ничего знать о производных типах. Тем не менее при вызове метода HurtHuman процесс вызовет метод для конкретного насекомого. Как он это делает? При помощи полиморфизма; название этого термина в переводе означает «способность иметь много форм». Процесс работает с обобщенным насекомым, но фактически он может работать и с любым конкретным типом насекомых, производным от него. Если объекты-насекомые реализованы по определенным правилам, то процесс будет вызывать правильный метод, и в этом ему поможет полиморфизм. Следовательно, основная выгода от полиморфизма заключается в том, что он позволяет работать на относительно общем уровне и при этом гарантирует правильную обработку частных случаев.
1.4 Объектно-ориентированное программирование Чтобы оценить преимущества ОО с точки зрения программиста, необходимо перейти от ОО к «объектно-ориентированному программированию», или ООП. Термин «программирование» в данном случае достаточно важен, поскольку методика ОО может применяться и на других стадиях разработки, не связанных с программированием. Аналогично, ООП также может применяться независимо от того, использовались ли в процессе разработки другие объектноориентированные приемы. Лично я полагаю, что максимальной пользы от объектноориентированных технологий можно добиться, если применять их на всех возможных стадиях разработки. С точки зрения программиста нам нужен язык, средствами которого можно было бы реализовать инкапсуляцию, наследование и полиморфизм. Подобные возможности есть во многих языках. Одни позволяют добиться более абстрактной, «чистой» реализации (что бы это ни означало), другие более практичны (то есть написанные на них программы работают с приемлемой скоростью). До настоящего момента самым распространенным языком ООП остается C++, пожалуй, его следует отнести скорее к практичным, нежели к абстрактным языкам. Именно на этом языке составлено большинство примеров для этой книги. Язык Java отчасти похож на него, хотя стоит немного ближе к абстрактным языкам. Java и C++ имеют много общего, и программы на Java с первого взгляда очень напоминают программы на C++. Некоторые примеры в этой книге написаны на Java. Рассмотрим фрагмент на C++:
class Insect { public: virtual void HurtHuman(void); }; class Wasp : public Insect { void HurtHuman(void); } Wasp *aWasp = new Wasp; Insect *anInsect = aWasp; anInsect -> HurtHuman(); В этом фрагменте определяются два класса, Insect и Wasp, причем Wasp является производным от Insect. Оба класса содержат метод HurtHuman. Затем мы создаем объект класса Wasp и сохраняем указатель на него в переменной aWasp. Наконец, мы создаем указатель на Insect и присваиваем ему адрес объекта Wasp. Таким образом, компилятор C++ считает, что anInsect ссылается на объект базового класса Insect, но на самом деле он ссылается на Wasp! Метод Insect::HurtHuman объявлен как виртуальный, поэтому при вызове метода HurtHuman через этот указатель C++ вместо реализации метода из класса Insect вызывает Wasp::HurtHuman. Перед нами типичный случай полиморфизма. Итак, мы знаем, что объект — нечто, обладающее определенными «правилами поведения» и интерфейсом (или набором интерфейсов) для работы с ними. Интерфейс обычно реализуется в виде набора методов, однако в некоторых случаях (например, в C++) возможна непосредственная работа с данными объекта.
www.books-shop.com
Но давайте вернемся к проблеме многократного использования кода и вкладу ОО в ее решение.
1.5 Многократное использование кода и объектная ориентация Возможности многократного использования кода, обусловленные ОО, не приходят сами собой — их приходится планировать. Разумеется, в C++ можно многократно использовать класс, создавая на его основе производные классы. Тем не менее сам класс должен стоить того — излишне специализированный класс мало кому нужен, однако от чересчур общего класса толку тоже будет немного. Основная проблема заключается в том, что такого рода многократное использование все равно тесно связано с исходными текстами. Изменение интерфейса какого-либо из базовых классов в иерархическом дереве приводит к катастрофе (так называемая проблема «неустойчивости базовых классов»). Я говорю об исходных текстах потому, что почти любой набор классов C++ («библиотека классов»), заслуживающий внимания, поставляется вместе с исходными текстами, чтобы пользователи могли нормально отлаживать свои приложения, а также использовать тексты функций базовых классов в своих разработках. Например, вам может потребоваться, чтобы способ открытия файлов MFC-программой в вашем приложении слегка отличался от стандартного. Вы берете исходный текст функции открытия файла, изменяете его и вставляете в свою функцию. Другая серьезная трудность заключается в том, что библиотека классов C++ может использоваться только в программах на C++ — в противном случае вам придется изрядно помучиться. Дело ухудшается привязкой библиотеки к конкретному компилятору. Чтобы использовать библиотеку классов с другим компилятором C++, ее нередко приходится компилировать заново. Конечно, библиотеки классов и их эквиваленты чаще всего могут использоваться только в том языке, на котором они были написаны. Как мы вскоре убедимся, COM-технология фирмы Microsoft этому правилу не подчиняется. Microsoft создала ее для того, чтобы любые программы, написанные на C++, Java, Visual Basic или любом другом языке, который обладает средствами для работы с COM, могли работать с набором определенных функций. Более подробная информация приведена ниже в этой главе. Итак, хотя при помощи ОО можно добиться большего уровня многократного использования, чем без нее, без ограничений дело не обходится. Преимущества ОО с точки зрения многократного использования программ лучше всего проявятся, если отложить программирование и поанализировать. Представив систему в виде набора объектов, можно довольно быстро определить, какие из этих объектов можно многократно использовать в данной системе или ее будущих расширениях и какие из них могут уже быть созданы кем-то другим в пригодной для использования форме.
Замечание Не забывайте о том, что объекты представляют собой «черные ящики» — вас интересует то, что они делают, а не то, как они это делают. Кроме того, немаловажно и то обстоятельство, что интерфейс этих «черных ящиков» остается неизменным.
1.5.1 Многократное использование на двоичном уровне Деление системы на логические компоненты составляет сущность «объектно-ориентированного анализа» (ООА). Полагаю, на этом же принципе основана работа компонентов — под этим невразумительным термином я понимаю «черные ящики», логически разделенные компоненты системы, которые могут использоваться несколькими процессами. На самом деле эти «черные ящики» представляют собой не что иное, как программные компоненты — то есть подключаемые модули, которые предоставляют стандартный набор функций через стандартный набор интерфейсов. Такие компоненты могут относиться к нескольким категориям: универсальные (например, связанные с выводом на печать и проверкой орфографии), диалоговые
www.books-shop.com
(переключатели и флажки), функциональные (работа которых подчинена некоторым деловым стандартам) и т. д. Тем не менее самое важное их свойство заключается в том,что их многократное использование ничем не ограничивается — вам не придется перекомпилировать их, вносить изменения в исходный текст или работать с определенным языком. Многократное использование такого рода чаще всего называется «двоичным». Технология, на которой основана работа таких объектов, может допускать расширение их функциональности и создание новых объектов, обладающих свойством многократного использования на двоичном уровне. Давайте представим себе, что нам поручено спроектировать новую систему для страховой компании. Мы уже смоделировали все данные и процессы и подошли к стадии объектноориентированного анализа. В ходе моделирования выяснилось, что в большом количестве процессов участвуют объекты нескольких видов: «клиенты», «условия», «страховые полисы» и «предложения». Приложение для составления договоров может пользоваться всеми четырьмя видами объектов, а для системы учета клиентов хватит объектов «клиент» и «полис». Вскоре выясняется, что некоторые из этих объектов должны использоваться сразу несколькими процессами, так что вам придется создавать их с учетом возможности многократного использования на логическом уровне. Если бы эти объекты представляли собой программные модули, то решить такую задачу, видимо, было бы не так уж сложно. К примеру, возьмем объект «клиент». Тщательный анализ поможет определить все интерфейсы, которые должны быть предоставлены всем пользователям таких объектов. Придется позаботиться и о том, чтобы в будущем к объекту «клиент» можно было добавить новые интерфейсы при возникновении каких-нибудь новых, непредвиденных требований. Тот же анализ может определить круг задач, выполняемых объектом, а моделирование данных покажет, с какими данными он должен работать (чаще всего эти данные представляются в виде таблиц и записей баз данных). Предоставляя такой объект каждому процессу, который должен работать с данными, мы добиваемся как минимум двух вещей: многократного использования, поскольку объект используется сразу несколькими процессами, и централизации, поскольку через объект должны проходить все данные о клиентах. Следовательно, в таких объектах можно реализовать основные положения вашего бизнеса и не сомневаться в их соблюдении — конечно, при условии, что все процессы, работающие с данными о клиентах, будут пользоваться этими объектами! Только подумайте: пользователи обладают полной свободой в получении нужной информации и работе с ней, при этом гарантируется соблюдение всех правил бизнеса — и все потому, что эти правила были воплощены в объектах, через которые осуществляется доступ к данным. О чем еще может мечтать разработчик информационных систем?
1.5.2 Другой пример многократного использования на двоичном уровне Давайте рассмотрим другую ситуацию, которая, на первый взгляд, не имеет ничего общего с тем, о чем говорилось раньше, но на самом деле представляет собой другой аспект той же самой проблемы. Представим себе окно диалога с множеством управляющих элементов — сеткой, флажками и парой кнопок. Некоторые из этих элементов предоставляются самой системой Microsoft Windows (чрезвычайно важный источник компонентов), а сетка взята из приложения для работы с электронными таблицами. Каждый элемент обладает определенным интерфейсом, его поведение подчиняется известным правилам, и при этом он достаточно универсален, чтобы работать в данном окне или любом приложении, в котором он потребуется.
1.6 Создание многократно используемых объектов Мы поговорили о том, как на концептуальном уровне происходит разделение системы на компоненты, но еще не выяснили, как это сделать. Какие механизмы применяются для объединения компонентов в систему? На жаргоне подобные механизмы называют «клеем», и разновидностей такого «клея» немало. Одни работают хорошо, другие дают вредные испарения, третьи ограничены определенными типами склеиваемых материалов. Время от времени происходит очередная технологическая революция, к чему крупные компании по производству «клея» оказываются совершенно не готовы. В подобных случаях компаниям приходится спешно перестраиваться, а несчастным авторам — переписывать морально устаревшие книги об искусстве склейки. Давайте рассмотрим несколько разновидностей такого «клея».
1.7 Нестандартные управляющие элементы Windows SDK Ⱦɚɧɧɚɹɜɟɪɫɢɹɤɧɢɝɢɜɵɩɭɳɟɧɚɷɥɟɤɬɪɨɧɧɵɦɢɡɞɚɬɟɥɶɫɬɜɨɦ%RRNVVKRS ɊɚɫɩɪɨɫɬɪɚɧɟɧɢɟɩɪɨɞɚɠɚɩɟɪɟɡɚɩɢɫɶɞɚɧɧɨɣɤɧɢɝɢɢɥɢɟɟɱɚɫɬɟɣɁȺɉɊȿɓȿɇɕ Ɉɜɫɟɯɧɚɪɭɲɟɧɢɹɯɩɪɨɫɶɛɚɫɨɨɛɳɚɬɶɩɨɚɞɪɟɫɭ[email protected]
Спецификация «нестандартного управляющего элемента» (custom control) была одним из первых видов «клея», который позволял Windows-программистам создавать специализированные управляющие элементы в окнах диалога. Данная спецификация определяет процесс взаимодействия элемента с Dialog Editor (инструментом для создания диалоговых окон в программах, написанных при помощи Microsoft Windows Software Development Kit, или SDK) и облегчает включение пользовательских управляющих элементов в стандартные диалоговые окна Windows. Тем не менее область применения таких элементов существенно ограничена: они могут использоваться лишь в программах, написанных на том же самом языке (что практически исключает все средства разработки, за исключением C и C++), и притом их возможности никак не назовешь богатыми. Работа таких элементов по сути дела ограничена посылкой сообщений, уведомляющих родительское окно о некотором событии (например, «меня щелкнули мышью»). Хотя многие нестандартные элементы существуют и в наши дни (скажем, пример MUSCROLL из старого Windows 3.x SDK), сейчас их почти никто не создает и уж тем более не берется продавать.
1.8 Microsoft Visual Basic и VBX Visual Basic — не только один из самых популярных видов «клея», но к тому же и самостоятельный язык программирования. Во время разработки проекта его создатели пришли к выводу, что в идее нестандартных элементов заложено большое будущее. Они решили обеспечить расширение возможностей Visual Basic посредством новой разновидности элементов, названных «нестандартными управляющими элементами Visual Basic», или, проще говоря, — VBX (по стандартному расширению файла, принятому для таких элементов). VBX представляют собой обычные DLL-библиотеки Windows, подчиняющиеся определенным требованиям (DLL, или библиотеки динамической компоновки, — программные модули, которые могут загружаться во время выполнения программы и совместно использоваться несколькими процессами). В состав профессионального выпуска Visual Basic 3.0 и более ранних версий входил пакет Control Development Kit (CDK), предназначенный для разработки VBX и ориентированный на язык C. Программист на Visual Basic, желающий воспользоваться VBX, должен включить VBX-файл в свой проект. В одном VBX-файле может храниться несколько нестандартных элементов. При включении файла в проект значки всех элементов появляются на панели элементов Visual Basic, чтобы воспользоваться ими, достаточно щелкнуть соответствующий значок и нарисовать элемент на форме. Остается лишь задать свойства элемента и написать код для обработки генерируемых им событий. «Свойствами» называются атрибуты элемента — например, его размеры или цвет фона. «Событиями» называются сообщения, при помощи которых элементы уведомляют объектконтейнер о выполнении некоторого условия. Сообщения могут быть как простейшими (например, «меня щелкнули мышью»), так и достаточно сложными (скажем, «вам пришло сообщение электронной почты»). Для VBX нельзя написать пользовательский метод, поэтому для таких случаев был выработан стандартный прием — методы имитировались при помощи так называемых «рабочих свойств», которые заставляли элемент выполнить некоторое действие при задании соответствующего значения свойства. Пожалуй, в этом проявилось некоторое отступление от объектной ориентации. Тем не менее концепция свойств оказалась довольно мощной — если в нестандартных элементах Windows SDK значения атрибутов (например, цвет) устанавливались при помощи runtime-кода, то в VBX они сохранялись между сеансами и никакого специального кода для этого не требовалось. С выходом каждой последующей версии Visual Basic функциональность VBX расширялась и достигла апогея в VBX версии 3.0, где также был реализован принцип связывания данных. Под «связыванием данных» понимается установление соответствия между элементом и полем базы данных — например, текстовое поле можно связать с хранящимся в базе полем «Имя». После того как значение в текстовом поле будет отредактировано пользователем, изменения могут быть пересланы в базу данных, и наоборот. Первоначально VBX были задуманы как нестандартные управляющие элементы примерно того же уровня, что и старые элементы SDK, — то есть визуальные объекты, дополнявшие стандартный набор управляющих элементов Microsoft Windows. Некоторые VBX входили в комплект Visual Basic: одни обеспечивали работу трехмерных управляющих элементов, другие предназначались для управления мультимедиа-устройствами, третьи позволяли работать с графикой и т. д. Многие VBX был написаны для Microsoft фирмами-подрядчиками, довольно часто эти фирмы продавали улучшенные варианты этих элементов. В наши дни на рынке представлены VBX самых разных типов — от нестандартных списков до полноценных текстовых редакторов и электронных таблиц или модулей, решающих сложные задачи современного бизнеса. Более того, многочисленные компании, работающие с Visual Basic, создали множество специализированных VBX для
www.books-shop.com
внутреннего использования. Например, в одном крупном английском банке применяется свыше 75 нестандартных VBX. Одна из причин стремительного роста популярности VBX заключалась в том, что Visual Basic превратился в единую среду разработки, в которой можно было работать с VBX. Но когда популярность VBX стала бесспорной, возникла потребность в других средствах разработки. Например, 16-разрядные VBX можно писать на Microsoft Visual C++ и многих других языках программирования. И все же уровень поддержки в них обычно оказывается ниже, чем в Visual Basic, поскольку для некоторых возможностей VBX требуется участие runtime-системы. VBX довольно близко подошли к реализации компонентной модели — они представляли собой «черные ящики», производимые в разных местах и предназначенные для решения широкого спектра задач. В статье, опубликованной в журнале Byte в начале 1994 года, утверждалось, что объектно-ориентированное программирование в этом отношении потерпело неудачу, а VBX преуспели. Бесспорно, рынок VBX оказался значительно шире, чем можно было предположить вначале. И все же модель VBX не идеальна. Во-первых, VBX являются расширениями Visual Basic, и, следовательно, их поддержка в других языках программирования оказывается более слабой. Вовторых, их формат запатентован и потому он может измениться (между форматами VBX в Visual Basic версий 1.0 и 3.0 действительно имеются отличия, хотя большей частью они совместимы). Втретьих, архитектура VBX в значительной степени привязана к процессорам семейства Intel 80x86, работающим в 16-разрядном режиме. Ближайшее будущее принадлежит 32-разрядным системам, причем необязательно на базе Intel. VBX должны легко переноситься на новые платформы, иначе они долго не проживут.
1.9 OLE! Фирма Microsoft рассмотрела возможности переноса VBX-технологии на различные платформы и вообще в 32-разрядный мир и поняла, что это будет непросто. Одновременно с этим в операционных системах и приложениях Microsoft начала доминировать технология, известная как OLE. Как мы увидим в главах 2 и 3, под термином OLE скрывается довольно много. Основное назначение OLE — служить «клеем» для взаимодействия объектов. На первый взгляд это именно то, что нужно! Тем не менее, когда возникла проблема переноса, в OLE отсутствовал стандартный механизм для асинхронного взаимодействия объекта с контейнером — все общение направлялось от контейнера к объекту. Для обработки событий потребовалось асинхронное взаимодействие в противоположном направлении.
Замечание Давайте уточним смысл термина: под «асинхронностью» я понимаю то, что объект может послать уведомление контейнеру тогда, когда считает нужным, а не только по требованию контейнера. Ни в коем случае не следует считать асинхронным сам процесс уведомления (это означало бы, что объект может продолжить работу сразу же после вызова функции для уведомления контейнера, не дожидаясь возвращения из нее). В Microsoft решили, что технология OLE должна быть заложена в основу нового поколения управляющих элементов — она замечательно «склеивает» объекты, переносится между различными платформами и работает как на 16-, так и на 32-разрядных системах. Новая спецификация элементов OLE была доработана и распространена среди разработчиков VBX в конце 1993 — начале 1994 года. К радости Microsoft, большинство разработчиков ее одобрило. Причины такого отношения были вполне понятны — новая спецификация существенно расширяла рынок, так как Visual Basic перестал быть единственным приложением, способным работать с управляющими элементами. Microsoft внедрила поддержку элементов OLE в другие инструментальные средства и деловые пакеты. Ожидалось, что крупные независимые фирмы последуют ее примеру. Первым серьезным приложением, в котором поддерживались элементы OLE, стал Microsoft Access версии 2.0, появившийся в апреле 1994 года. На этот момент спецификация была еще не закончена, поэтому по функциональности элементов Access уступал Visual Basic версии 4.0 или Visual FoxPro версии 3.0, выпущенным в 1995 году. Затем в конце 1995 года появился Visual C++ 4.0, который не только стал основным средством для создания
www.books-shop.com
элементов OLE, но и впервые использовал их. Вскоре после этого разработчики стали более серьезно относиться к элементам OLE. Спецификация элементов OLE 1996 года, в которой была впервые представлена концепция «внеоконных» элементов, была выпущена ранее намеченного срока и получила название «OCX 96». Элементы OLE превратились в элементы ActiveX (кстати говоря, X не произносится!), и поддержка последних была внедрена в Microsoft Internet Explorer. Более того, технология COM, на которой была основана работа OLE и ActiveX (о COM, или модели компонентных объектов, мы узнаем в следующей главе), позволила Microsoft перекинуть мост между элементами ActiveX и такими языками, как Visual Basic и Java — отныне эти языки могли использоваться для разработки элементов и работы с ними.
1.10 Интерфейсы и включение Технология ActiveX будет достаточно подробно рассмотрена в нескольких ближайших главах. Сейчас хотелось бы подчеркнуть, что ActiveX в первую очередь представляет собой спецификацию интерфейсов для взаимодействия между объектами. Вместо «наследования», при котором интерфейсы элементов должны оставаться постоянными на каждом уровне иерархии, в ActiveX используется «включение» (containment) — методика, позволяющая одному объекту «вобрать в себя» другой объект и предоставить любое количество интерфейсов внутреннего объекта в качестве своих собственных. Кроме того, новые возможности объекта можно реализовать в виде нового интерфейса и избежать модификации объекта. Поскольку тот или иной интерфейс может поддерживаться как внутренним, так и внешним объектом, ActiveX фактически обеспечивает полиморфизм.
1.11 Automation По своим возможностям элементы ActiveX по крайней мере не должны уступать существующим VBX, поэтому в них должна быть реализована поддержка свойств и событий. Одним из ключевых принципов их работы является технология Automation, ранее называвшаяся «OLE Automation». Она позволяет одной программе управлять выполнением другой посредством установки/чтения свойств объектов и вызова их методов. Использование этой технологии означает, что элементы ActiveX могут обладать нестандартными, или пользовательскими методами. Более того, для элементов ActiveX должно быть реализовано связывание данных, возможность быть невидимыми во время работы программы (некоторые элементы не обладают визуальным представлением) и ряд других возможностей, о которых мы узнаем по ходу книги. Успех элементов ActiveX зависел от многих факторов. Разумеется, прежде всего элементы ActiveX обязаны делать все то, что делают VBX. Их применение в программах должно быть таким же простым, как и для VBX. Элементы ActiveX должны создаваться без особых трудностей, причем хотелось бы иметь простой способ преобразования элементов VBX в ActiveX. Желательно, чтобы элементы ActiveX поддерживались как можно большим количеством средств разработки и приложений — это приведет к расширению рынка. Большая часть этих факторов была учтена в пакете OLE Controls Developer’s Kit (OLE CDK), входившем в комплект Visual C++ версий 2.0 и выше. В 1996 году Microsoft выпустила ряд других средств для создания элементов ActiveX. Вскоре и другие фирмы стали внедрять поддержку создания и применения элементов ActiveX в своих инструментальных пакетах. Элементы ActiveX в полной мере воплощают идею компонентов: они представляют собой механизмы для создания многократно используемых объектов, способных работать с различными средствами разработки и многими языками программирования. Они решают множество задач и расширяются посредством введения новых интерфейсов, не требуя наследования. Новые программные средства, выпущенные Microsoft и другими фирмами в 1996 году, позволяют писать элементы ActiveX не только на C++, но и на других языках. Несомненно, будущее сулит компонентам еще более светлые перспективы. Представьте себе реализацию Microsoft Office на основе элементов ActiveX, причем каждый элемент может самостоятельно использоваться любым количеством приложений. Процесс создания программных продуктов обогатится совершенно новым modus operandi, особенно для разработчиков
www.books-shop.com
коммерческих программ. Ключевым моментом здесь становится лицензирование, поскольку создатели таких компонентов, вероятно, пожелают получить деньги за свой труд. В главах 3 и 16 приведены подробности того, как элементы ActiveX помогают разработчикам в этом отношении. Программы-компоненты также облегчают жизнь программистам, которые могут взять набор готовых блоков и построить из них требуемое решение. Нельзя обойти вниманием и влияние Internet (точнее, World Wide Web). Элементы ActiveX должны стать одним из ключевых компонентов для работы с интерактивной информацией в Web. Следовательно, их размер должен быть минимальным, а скорость работы — максимальной. Кстати, это стало одной из причин, по которой OCX 96 считается важной вехой на пути развития элементов ActiveX — некоторые положения, заложенные в этой спецификации, помогают уменьшить и ускорить элементы ActiveX. По этой же причине некоторые новые средства для создания элементов ActiveX основаны на принципе «меньше и быстрее», а не на принципе «как можно проще». Разумеется, в категории программ-компонентов элементы ActiveX не одиноки. Существует немало других технологий, обладающих примерно теми же возможностями и выполняющих аналогичные задачи. Каждая разновидность программ-компонентов обладает своими достоинствами и недостатками, и наш выбор должен учитывать множество коммерческих и технических факторов. Если провести аналогию с автомашинами, то элементы ActiveX похожи на общепринятый, но не единственный стандарт. Другие производители машин могут изменить расположение педалей или переместить место водителя на другую сторону (для меня как англичанина это особенно близко).
1.12 Будущее? Я не могу завершить эту главу, не уделив внимания Java — языку, созданному фирмой Sun Microsystems, фавориту Internet и лидеру в сфере web-броузеров. На первый взгляд можно подумать, будто апплеты Java непосредственно конкурируют с элементами ActiveX. Я с этим не согласен (и Microsoft — тоже). Думаю, нетрудно представить себе апплеты Java, которые управляют работой элементов ActiveX или попросту являются ими. Эти технологии по сути дела дополняют друг друга. Как только основные производители компиляторов выпустят средства, которые обеспечат взаимозаменяемость Java и ActiveX (а такие средства вскоре появятся — возможно, уже к моменту, когда вы будете читать эту книгу), ситуация с выбором компонентов заметно упростится. Но кто я такой, чтобы предсказывать будущее? На момент подготовки первого издания этой книги элементы ActiveX считались чем-то новым и невиданным. После выхода книги они стали вполне рядовым явлением, однако я так и не смог предсказать ни широкого ассортимента средств для их создания, ни неожиданно быстрого распространения всевозможных дополнений и изменений в спецификации. Я даже не предвидел их переименования — из элементов OLE в OCX и затем в элементы ActiveX.
1.13 Создание элемента ActiveX Настало время создать наш первый элемент Smile. Я перечислю все необходимые действия, но не стану подробно разъяснять их, поскольку этим мы займемся в нескольких ближайших главах. Мое единственное намерение — показать вам, как легко написать элемент ActiveX на C++ и воспользоваться им в программе. В данном случае используется библиотека MFC версии 4.0 и выше, входящая в комплект Visual C++ и других компиляторов С++. Хотя существует множество других способов для создания элементов ActiveX на других языках (некоторые из них будут рассмотрены в книге позднее), я воспользовался MFC, поскольку благодаря этой библиотеке наш пример на C++ становится особенно простым, а большая часть книги посвящена именно C++.
1.14 Требования к компьютеру На вашем персональном компьютере должна быть установлена операционная система Microsoft Windows 95 или Windows NT 4.x, а также все необходимые аппаратные средства, необходимые для нормальной работы Visual C++. Предполагается, что у вас установлен Visual C++ версии 4.x. Если вы работаете с более ранними версиями Windows, то все выполняемые действия не изменятся, хотя несколько изменится внешний вид созданного элемента. Если же у вас установлена версия Visual C++ выше 4.0, то сущность действий останется прежней, хотя сочетание «элемент OLE» придется в нескольких местах заменить сочетанием «элемент ActiveX».
www.books-shop.com
1.15 Создание элемента-примера Smile Я воспользуюсь примером элемента ActiveX, написанным моим начальником Эриком Лангом (Eric Lang) и опубликованном в журнале Microsoft Systems Journal. Для тех, кто не читал первого издания — я был просто потрясен гениальностью этого кода (возможно, я получу повышение)! Читателям первого издания — никакого повышения я не получил, так что теперь могу спокойно сказать, что пример на редкость убогий. Пример содержится в каталоге \CODE\CHAP01\SMILE на прилагаемом CD-ROM. Скопируйте содержимое каталога на жесткий диск и выполните следующие действия: 1. 2. 3. 4. 5. 6. 7. 8. 9.
Выполните команду File|Open Workspace и откройте файл проекта SMILE.MDP. Выполните команду Build|Rebuild All и подождите, пока закончится компиляция и компоновка. Из меню Tools запустите приложение OLE Control Test Container. В Test Container выполните команду Edit|Insert OLE Control и выберите из предложенного списка Smile Control. Обратите внимание на элемент Smile, появившийся в окне Test Container. Щелкните на нем, чтобы убедиться в его выделении. Откройте протокол событий Test Container командой View|Event Log. Теперь проследите за событиями, щелкая в различных частях элемента и нажимая различные клавиши при выделенном элементе. Просмотрите свойства элемента командой View|Properties. Немного поэкспериментируйте с изменением величин и посмотрите, что при этом происходит. Просмотрите и измените свойства элемента при помощи перечня свойств. Для этого выполните команду Edit|Properties|Smile Control Object. Проследите за тем, как меняется внешний вид элемента при изменении значения свойства Sad в диалоговом окне Test Container’s Properties (значение 0 соответствует FALSE, а –1 — TRUE). При работе с перечнем свойств достаточно установить или снять соответствующий флажок.
Вот и все! Все остальное будет рассказано по мере изложения материала.
1.16 Что дальше? Мы только что создали первый элемент ActiveX и протестировали его в приложении Test Container. Хотя на первый взгляд все было очень просто, за кулисами происходило довольно много событий, обеспечивавших работу элемента. В главе 2 приведен обзор технологий COM и ActiveX; в ней очень кратко затронуты наиболее важные темы. Глава 3 более подробно останавливается на аспектах COM, важных для создания управляющих элементов, в том числе и на тех аспектах, которые непосредственно обеспечивают их работу. Итак, что же читать дальше? Приведу несколько рекомендаций:
Если вы хотите лучше представить себе принципы работы элементов, прочитайте главы 2 и 3. Если вас в первую очередь интересует создание элементов, а не технические подробности, главы 2 и 3 можно пропустить. Если вы хотите познакомиться с кратким обзором COM и ActiveX, не углубляясь в высокие материи, прочитайте главу 2,но пропустите главу 3. Если вы хотите больше узнать о библиотеке MFC, прочитайте приложение А — в нем содержится довольно подробное введение в тему.
www.books-shop.com
Глава
2
ActiveX и OLE: основные положения В главе 1 было сказано, что технология ActiveX предназначена для «склеивания» объектов. Теперь мы остановимся на этой теме более подробно. Данная глава не претендует на роль учебника по ActiveX и OLE. Если вам нужен учебник, я бы посоветовал обратиться к книге Крейга Брокшмидта (Kraig Brockschmidt) «Inside OLE» издательства Microsoft Press — в ней содержится лучшее введение в тему. Я лишь намерен кратко пройтись по основным моментам, чтобы быть уверенным в том, что мы одинаково понимаем некоторые базовые концепции и термины. Нам необходима полная ясность во всем, что действительно важно для построения элементов ActiveX. Некоторым аспектам ActiveX и OLE в этой главе уделено особое внимание, поскольку они чрезвычайно важны для элементов ActiveX, а читатели могут недостаточно четко понимать их. В других областях я ограничиваюсь кратким обзором и задерживаюсь лишь на тех аспектах, которые имеют непосредственное отношение к нашей книге.
ЗАМЕЧАНИЕ Если вас не интересует, что такое ActiveX и OLE и на каких принципах работают эти технологии, можете пропустить эту и следующую главы — полезные и функциональные элементы ActiveX можно создавать и без них. С другой стороны, каждый, кто интересуется техническими подробностями, найдет в главах 2 и 3 много полезной информации.
ЗАМЕЧАНИЕ В марте 1996 года фирма Microsoft анонсировала новое семейство технологий, получивших общее название «ActiveX». К этому семейству относятся многие технологии семейства OLE, поэтому в терминологии произошли многочисленные изменения — например, элементы OLE были переименованы в элементы ActiveX. По этой причине термин OLE в настоящем издании книги был во многих случаях заменен на ActiveX. Однако в ряде случаев такая замена была невозможна и не имела смысла в контексте материала (например, при исторических ссылках на OLE). Так что если в одном месте вы видите термин OLE, а в другом — ActiveX, для этого может быть веская причина… а может, дело в обычной рассеянности автора.
Некоторые темы, заслуживающие более пристального внимания (например, Automation и усовершенствования в элементах ActiveX и OLE), рассмотрены в этой главе на том же уровне, что и другие, однако в главе 3 они раскрываются значительно подробнее.
2.1 COM Начнем с концепции, лежащей в основе ActiveX и OLE, — модели компонентных объектов, или COM. Спецификация COM представляет собой базовый протокол, описывающий процесс взаимодействия объектов. Она определяет, что следует считать «COM-объектом», как создаются экземпляры таких объектов и когда они перестают существовать. ActiveX и OLE — всего лишь две разновидности сервиса, построенного на базе COM и пользующегося его возможностями. В основе COM (а следовательно, и ActiveX с OLE) лежит идея «интерфейса», то есть контракта (соглашения) между объектом и его пользователем (рис. 2-1). ActiveX и OLE поддерживают
www.books-shop.com
множество интерфейсов; разработчики могут по своему усмотрению добавлять к ним другие. С точки зрения программиста интерфейс представляет собой список точек входа для заданного набора процедур, или, в переводе на язык C, — массив указателей на функции. COM-объекты и их данные остаются закрытыми для окружающего мира — не существует такого понятия, как указатель на COM-объект. Вместо этого COM-объекты предоставляют указатели на свои интерфейсы. По мере изложения материала я покажу, что это значит.
Рис. 2-1. Интерфейс объекта (подобное обозначение является стандартным для ActiveX) Действуя в качестве «клея для объектов», ActiveX и OLE реализуют довольно много интерфейсов и определяют еще больше. Реализация интерфейса означает, что ActiveX и OLE содержат программный код для выполнения действий, для которых предназначены входящие в интерфейс функции. Возможно, вам приходилось пользоваться OLE для внедрения диаграмм Microsoft Excel в документы Microsoft Word. Главы 2 и 3 наглядно показывают, что область применения ActiveX значительно шире простого внедрения объектов. Я лишь перечислю наиболее важные технологии, входящие в семейство ActiveX, чтобы вы убедились в богатстве их возможностей. Технология
Описание
Automation
Способность приложения на программном уровне управлять объектами другого приложения. Неотъемлемой частью Automation является способность объекта описывать свои возможности посредством описания типов. Именно Automation лежит в основе работы элементов ActiveX.
Документы OLE
Все возможности OLE, связанные с документами, — связывание и внедрение, dragand-drop и визуальное редактирование.
Документы ActiveX
(Ранее назывались «Doc-объектами».) Существенное расширение объектов OLE; в частности, позволяет распространить связанный или внедренный объект на несколько страниц, а также передать ему право определять печатный вид этих страниц. Кроме того, позволяет приложению выступать в роли «подшивки» для разнородных, но логически связанных объектов.
Элементы ActiveX
Объекты, которые могут обладать визуальным представлением и управляются на уровне языка программирования. Элементы ActiveX могут поддерживать средства активизации документов ActiveX, а также могут быть «внеоконными» (то есть не иметь визуального представления). Такие объекты пользуются средствами Automation для раскрытия свойств и методов, а также для обработки событий.
Сообщения ActiveX
Единый интерфейс для обмена сообщениями (основанный на средствах Automation).
ActiveX Schedule+
Интерфейс Automation для планирования деятельности в рабочих группах.
DCOM
Организация выполнения объекта на одном компьютере под управлением другого компьютера.
Транзакции ActiveX
Набор интерфейсов, позволяющих приложению координировать прохождение транзакции на нескольких серверах.
OLE DB
Набор интерфейсов, предоставляющих приложениям основные услуги баз данных для широкого диапазона типов данных.
Сценарии ActiveX
Возможность записи объектов в виде сценариев на таких языках, как Visual Basic Scripting Edition или JavaScript.
OLE (а теперь и ActiveX) постепенно превращается в стандарт как системных, так и пользовательских интерфейсов, поэтому со временем наверняка появятся и другие технологии с названиями «Что-нибудь ActiveX». ActiveX — развивающаяся технология, которую любой программист может расширить для своих целей, не теряя совместимости с более ранними поколениями сервиса OLE. Как показано на рис. 2-2, ActiveX состоит из множества компонентов и интерфейсов. Поначалу эта схема выглядит довольно устрашающе, однако ряд обстоятельств оборачивается в вашу пользу:
www.books-shop.com
Некоторые из этих интерфейсов уже реализованы на уровне ActiveX или в инструментах, которые применяются при разработке приложений — следовательно, вам остается только пользоваться ими в своих программах. Для создания полноценных объектов ActiveX достаточно небольшого числа интерфейсов. Библиотеки классов C++ (например, Microsoft Foundation Classes, или сокращенно MFC) берут на себя почти всю грязную работу. ActiveX нередко можно рассматривать как очередной аспект программирования на C++ для Microsoft Windows. В некоторых языках высокого уровня (таких, как Microsoft Visual Basic и Java) можно создавать объекты (в том числе и элементы) ActiveX без какого-либо программирования на C или C++. В будущем эта тенденция будет развиваться, в связи с этим можно ожидать появления разнообразных инструментов для создания объектов ActiveX.
Рис. 2-2. Строительные блоки ActiveX и важнейшие интерфейсы
2.2 IUnknown В основе всех COM-интерфейсов лежит интерфейс с именем IUnknown. Имена всех COMинтерфейсов подчиняются стандартному обозначению — имя интерфейса начинается с буквы I, за которой следует собственно имя. Например, интерфейс, который вам хотелось бы назвать MyInterface, должен называться IMyInterface. IUnknown содержит указатели на три функции: AddRef, Release и QueryInterface. Любой COM-интерфейс должен содержать методы интерфейса IUnknown. Отсюда нетрудно перейти на терминологию C++ — если представить интерфейс в виде абстрактного базового класса (чем он, в сущности, и является), то все интерфейсы являются производными от IUnknown. IUnknown можно представить в следующем виде:
class IUnknown { public: virtual ULONG AddRef(void) = 0;
www.books-shop.com
virtual ULONG Release(void) = 0; virtual HRESULT QueryInterface(REFIID riid, LPVOID FAR *ppv) = 0; }; Пока мы обойдемся без подробных объяснений. Важно заметить, что IUnknown содержит объявления этих трех методов. Любой интерфейс представляет собой абстрактный базовый класс, то есть не содержит ничего, кроме чисто виртуальных функций — поскольку он представляет собой простую таблицу указателей на функции. Программист должен самостоятельно позаботиться о фактической реализации этих функций или, во многих случаях, воспользоваться чьей-то реализацией. Для класса, содержащего одну или несколько виртуальных функций, в C++ создается «виртуальная таблица» (также называемая v-таблицей). На самом деле эта таблица представляет собой массив указателей на функции, то есть в точности совпадает с программной реализацией интерфейса. V-таблица содержит адреса различных функций, производных от абстрактного базового класса. При работе на C или другом языке низкого уровня v-таблицы приходится создавать вручную; что еще хуже, их необходимо заполнить адресами функций. Эта работа не так уж сложна, однако ее все же лучше возложить на компилятор C++ (самое время напомнить фанатикам C, что их любимый язык мертв, а его место давно занял C++). Разумеется, в Java предусмотрены аналогичные средства, а в языках типа Visual Basic все эти подробности скрыты от программиста. Три метода интерфейса IUnknown являются ключевыми для всей COM-технологии. Методы AddRef и Release предназначены для подсчета ссылок и в конечном счете определяют длительность существования COM-объекта. Метод QueryInterface предоставляет базовый механизм, при помощи которого пользователь COM-объекта определяет, что может сделать объект (точнее, какие интерфейсы он поддерживает). QueryInterface возвращает указатель на интерфейс. Если у вас имеется указатель на IUnknown и вы хотите получить указатель на интерфейс объекта с именем IMyInterface, необходимо воспользоваться методом IUnknown::QueryInterface. Аналогично, при помощи метода IUnknown::QueryInterface можно получить указатель на IUnknown или любой другой интерфейс, поддерживаемый объектом.
Когда QueryInterface не работает Программист, определяющий реализацию объекта, может запретить возврат указателя на определенный интерфейс функцией QueryInterface. Как мы убедимся в следующей главе, в элементах ActiveX это происходит довольно часто. В таких случаях объект обычно обладает своим собственным механизмом для обращения к таким «скрытым» интерфейсам или намеренно ограждает их от внешних пользователей. Следует учитывать два важных момента, относящихся к QueryInterface:
При получении указателя на IUnknown запрос QueryInterface, обращенный к любому интерфейсу, поддерживаемому объектом, должен возвращать одинаковое значение указателя IUnknown (рис. 2-3). Это позволяет программисту узнать, относятся ли два интерфейсных указателя к одному и тому же объекту, — для этого достаточно сравнить два значения, полученных в результате вызовов QueryInterface для IUnknown по двум указателям. Следовательно, хотя все остальные интерфейсы содержат методы IUnknown и их можно рассматривать как IUnknown, лишь один из них выступает в роли того, что я временно назову «главным IUnknown». При вызове QueryInterface для интерфейса, не поддерживаемого объектом, возвращается код ошибки. Таким образом, во время выполнения программы можно определить возможности объекта в смысле поддерживаемых им интерфейсов.
Необходимо понимать, что интерфейс представляет собой контракт между создателем объекта и его пользователем. Если объект реализует данный интерфейс и предоставляет доступ к нему, пользователь может быть уверен,что объект поддерживает все методы интерфейса и сохраняет их семантику. Кроме того, после определения интерфейса его уже нельзя изменить. Такое изменение привело бы к нарушению контракта и, следовательно, нарушениям в работе любых приложений или объектов, которые использовали или реализовали данный интерфейс. Если же потребуется внести какие-то изменения, приходится создавать новый интерфейс. По мере изложения материала этой и последующей глав я покажу несколько новых интерфейсов и объясню, зачем они были созданы.
www.books-shop.com
Рис. 2-3. Отношения между интерфейсами и QueryInterface Метод QueryInterface получает два параметра, о которых я до настоящего момента умалчивал. Первый параметр представляет собой REFIID (ссылку на идентификатор интерфейса) — служебный тип данных COM, с которым нам предстоит близко познакомиться. Значение REFIID определяет конкретный интерфейс, который вам нужен. Второй параметр предназначен для хранения возвращаемого интерфейсного указателя. Сначала мы более подробно рассмотрим подсчет ссылок, а затем займемся REFIID и посмотрим, что это такое.
2.3 Подсчет ссылок Когда вы получаете от объекта интерфейсный указатель, предоставивший его механизм обычно вызывает метод AddRef — тем самым он сообщает интерфейсу о наличии «пользователя». При создании новых указателей на данный интерфейс значение его счетчика ссылок возрастает. Если же интерфейс становится ненужным, пользователь должен вызвать для него метод Release. Происходит уменьшение счетчика. В нормальной ситуации объект живет до момента, когда счетчики ссылок для всех его интерфейсов не будут обнулены. Как я уже говорил, интерфейсы часто реализуются в виде классов C++; в простейшем варианте при каждом получении указателя на интерфейс может создаваться новый экземпляр класса. Следовательно, каждый объект должен сам следить за своим счетчиком ссылок и уничтожить себя, когда в нем отпадет надобность. Это означает, что любая реализация интерфейса должна сама заниматься подсчетом своих ссылок. Впрочем, возможен и другой подход. Если интерфейсы реализованы так, что создание нового интерфейсного указателя не сопровождается созданием нового объекта (например, за счет вложенных классов C++), то подсчет каждым интерфейсом своих пользователей оказывается неэффективным. В таких случаях стоит поручить все вызовы AddRef и Release главному интерфейсу IUnknown объекта. Подобный подход принят в библиотеке MFC и в большинстве программ-примеров этой книги. При этом сам объект знает о том, сколько у него пользователей, и может определить момент своей смерти — для этого ему достаточно при каждом вызове Release сравнивать количество пользователей с 0. Затем объект может выполнить необходимые «предсмертные» действия. Например, если объект находится внутри выполняемого файла, то он может закрыть свое окно и прекратить существование. Если объект реализован в виде библиотеки динамической компоновки (DLL), он не может выгрузить себя из памяти. Вместо этого DLL-библиотека запоминает свое состояние, чтобы при необходимости COM смог освободить ее. В других языках могут быть предусмотрены специальные средства, которые скрывают от вас все эти детали. Например, в Java имеется встроенный механизм сборки мусора, который сам занимается подсчетом ссылок. Кроме того, один из механизмов Java был позаимствован Microsoft для реализации QueryInterface — речь идет о преобразовании типов. У программиста появляется возможность получить новый интерфейс в виде класса, преобразовав его к нужному типу; возвращаемое значение NULL свидетельствует о том, что интерфейс не поддерживается данным объектом. В настоящее время метод IUnknown::QueryInterface является самым распространенным местом для создания новых интерфейсных указателей. Если QueryInterface успешно возвратил интерфейсный указатель, можно быть уверенным в том, что для него были соблюдены правила подсчета ссылок и вызван AddRef. Следовательно, после завершения работы с указателем необходимо вызвать для него метод Release.
Некоторые правила подсчета ссылок и его оптимизации приведены в спецификации «OLE 2 Programmer’s Reference, Volume One» (Microsoft Press). Основная идея заключается в том, что подсчет ссылок должен осуществляться для всех интерфейсных указателей, особенно при создании их локальных копий. Исключение составляют лишь те случаи, когда пользователь интерфейса абсолютно уверен в том, что без вызова AddRef можно обойтись — например, если указатель на интерфейс создается и используется исключительно на протяжении жизненного цикла указателя на другой интерфейс того же объекта. Не уверен — подсчитывай ссылки! Подсчет ссылок обычно не вызывает никаких осложнений, однако некоторых ситуаций следует избегать. Особенно неприятными оказываются циклические ссылки, при которых один объект ссылается на другой (прямо или косвенно, через промежуточный объект), а тот, в свою очередь, содержит ссылку на первый. Если ни один из этих объектов не вызывает Release для интерфейса другого объекта и дожидается, пока его счетчик ссылок станет равным 0, возникает взаимная блокировка (deadlock). Разумеется, возможный выход заключается в том, чтобы избегать подобных ситуаций, но это не всегда реально. Кроме того, можно распознать подобный «симбиоз» и схитрить, заставив один объект отказаться от подсчета ссылок на другой.
2.4 Другой способ определения возможностей объекта ActiveX предоставляет объектам другой способ для описания своих возможностей, не связанный с накладными расходами на создание объектов и позволяющий определить функции объекта на более высоком уровне — например, выяснить, умеет ли конкретный объект связываться с источником данных. Речь идет о так называемых «компонентных категориях». В нескольких словах, компонентная категория представляет собой запись реестра (registry), которая описывает набор возможностей объекта. Потенциальный пользователь может заглянуть в реестр и посмотреть, поддерживает ли объект необходимую компонентную категорию. Если поддерживает, то можно создавать экземпляр объекта. Некоторые компонентные категории определяются на уровне ActiveX, однако эта методика является стопроцентно расширяемой, так что вы можете по своему усмотрению создавать новые категории. Скажем, можно создать набор объектов, которые умеют готовить кофе: создав компонентную категорию CanMakeCoffee, вы позволите приложениям, работающим с вашими объектами, узнать об этой возможности. О значении компонентных категорий для элементов ActiveX рассказано в последней главе этой книги.
2.5 REFIID, IID, GUID и CLSID У читателя может возникнуть законный вопрос: так как же создать указатель на интерфейс объекта? Ответ откладывается до раздела «Мой первый интерфейсный указатель» на стр. 57, тем не менее во время нашего обсуждения IUnknown упоминалось понятие REFIID (ссылки на интерфейсный идентификатор, или IID), которое оказывается достаточно важным при создании указателей. REFIID представляет собой ссылку в терминах C++ (или указатель в терминах C) на IID. Все без исключения интерфейсы, открываемые объектом, должны обладать IID. Формат IID определяется в спецификации COM, для их создания применяются GUID. Сокращение GUID означает «глобально-уникальный идентификатор», причем «глобальность» следует понимать буквально. Основной принцип заключается в том, что GUID, сгенерированный где угодно и кем угодно, не должен совпасть с другим GUID, созданным кем-то в другом месте. Можно трактовать GUID как очень большие числа, которые однозначно определяют что-либо, в данном случае — интерфейсы (см. ниже врезку «GUID и UUID»).
GUID и UUID для любознательных Те, кому приходилось иметь дело со стандартом OSF DCE RPC (сокращение, которое означает «удаленный вызов процедуры в распределенных компьютерных системах Open Systems Foundation»!), знают GUID под именем UUID, или «вселенски-уникальных идентификаторов». В сущности, это одно и то же. GUID представляет собой структуру следующего формата:
struct GUID { DWORD Data1; WORD Data2; WORD Data3;
www.books-shop.com
BYTE Data4[8]; } Если сложить количество байт, выясняется, что GUID состоит из 16 байт. Новые GUID создаются функцией COM CoCreateGuid. Для создания GUID существуют различные средства — например, утилита GuidGen, входящая в Win32 SDK и Microsoft Visual C++ версий 2.0 и выше, а также AppWizard из Visual C++. Но не стоит волноваться: алгоритм, по которому создается GUID, гарантирует, что у вас не возникнет проблем с повтором значений! Один из главных архитекторов OLE, Тони Уильямс (Tony Williams), следующим образом поясняет алгоритм создания GUID в своем сообщении электронной почты: «В работе функции CoCreateGuid используется идентификатор компьютера (уникальность в пространстве) и значение текущего времени с высоким разрешением(уникальность во времени) с дополнительными битами, которые учитывают возможность обратного перевода часов и т. д. Функция работает по алгоритму OSF DCE. Идентификатор компьютера представляет собой сетевой адрес, если компьютер подключен к локальной сети; в противном случае используется другой алгоритм для генерации набора битов, который с высокой вероятностью оказывается уникальным для данного компьютера и заведомо не совпадет с другим сетевым адресом». При ближайшем рассмотрении GUID представляет собой 16-байтовое число, обычно записываемое в виде последовательности из 32 шестнадцатеричных цифр в так называемом «переносимом формате» — например, {37D341A5-6B82-101B-A4E3-08002B291EED}. Отдельные «поля» внутри этой записи, равно как и сама структура GUID, не обладают самостоятельным значением, и это число всегда следует рассматривать в целости.
Как упоминалось выше, каждый COM-интерфейс обладает некоторым значением IID. «Идентификатор класса», или сокращенно CLSID, представляет собой тип (класс) объекта ActiveX, иногда называемого «вспомогательным классом». Если вернуться к нашей иерархии насекомых из главы 1, то согласно правилам COM насекомые вообще, осы, мухи, муравьи и пчелы должны иметь свои CLSID. Немного позже я покажу, какое место занимает CLSID в общей картине.
DCOM, удаленный доступ,маршалинг и распределенные интерфейсы Одна из самых замечательных особенностей COM — это относительная легкость, с которой реализация интерфейса может быть «удалена» от точки исполнения. Это означает, что реализация интерфейса живет и работает не на компьютере пользователя, а в каком-то другом месте. Этот механизм основан на спецификации DCOM (распределенного COM). Фирма Microsoft всегда обещала, что вам не придется вносить изменения в написанные объекты для того, чтобы обеспечить возможность их удаленного использования. Теоретически это обещание не так уж сложно сдержать, поскольку такая возможность была учтена во время проектирования COM, OLE и ActiveX (на практике, конечно, перенос реализации интерфейса на другой компьютер сопряжен с огромным количеством проблем). Летом 1996 года Microsoft начала выполнять обещанное и сделала DCOM частью Windows NT 4.0. Хотя DCOM и не относится непосредственно к созданию элементов ActiveX (обычно они представляют собой небольшие компоненты, и выполнение их на компьютере пользователя оказывается значительно более эффективным), в этой врезке я не ограничиваюсь простым упоминанием о DCOM и объясняю некоторые базовые принципы его работы. Вызов метода интерфейса — не что иное, как обычный вызов функции. Следовательно, если передать вызов метода RPC-посреднику (напомню, что RPC означает «вызов удаленной процедуры») — то есть обратиться к «фиктивной» реализации, — то параметры будут переданы на сервер в стандартном формате DCE (распределенной компьютерной системы) RPC. На сервере имеется RPC- заглушка, которая обращается к настоящему интерфейсу настоящего объекта. RPCзаглушка получает результат и передает его по сети клиенту. Для программы все выглядит так, словно она работает только с посредником. Аналогично, с точки зрения сервера, клиентом в такой ситуации является RPC-заглушка. Из всего сказанного следует, что факт удаления реализации для пользователя не имеет никакого значения — разве что вызовы методов обрабатываются несколько дольше. Реализация
www.books-shop.com
интерфейса также не обязана знать о том, что пользователь находится на другом компьютере. Разумеется, ничто не мешает пользователю и реализации работать на компьютерах с разной архитектурой процессоров или даже с совершенно разными операционными системами. Мое объяснение упрощено до предела. Основная мысль состоит в том, что COM поддерживает концепцию, а на уровне DCOM — воплощает в жизнь удаленную реализацию интерфейсов. К тому же дело не ограничивается интерфейсами, созданными в Microsoft. Существуют инструменты, которые позволяют организовать удаленный доступ к любому интерфейсу, определяемому программистом. Сделать это не так уж сложно; основное, что от вас требуется — определить интерфейс так, чтобы с ним могли работать системные средства организации удаленного доступа. Для этого создается специальный «файл описания интерфейса», написанный на расширении стандартного «языка описания интерфейсов» (IDL), который описан ниже в этой главе и используется на протяжении всей книги. Обратите внимание на то, что Microsoft не пытается сделать удаленным программный интерфейс Windows. Следовательно, для внедрения диаграммы Microsoft Excel в документ Microsoft Word необходимо, чтобы Excel работал на одном компьютере с Word (ходят слухи, что это ограничение снимается в некоторых продуктах независимых фирм). Одно из основных понятий из области удаленных интерфейсов, в высшей степени важное для работы COM, иногда называется «маршалингом». В него входит обмен данными между процессами (и как следствие — возможность обмена данными по сети) и потоками в стандартном формате, который после выполнения «демаршалинга» может быть понят приемником. Маршалинг используется во всех интерфейсах (не считая нескольких мелких исключений), как удаленных, так и локальных, потому что взаимодействие между локальными процессами в COM организуется на базе разновидности RPC, которая называется LRPC (упрощенный вызов удаленной процедуры). Чаще всего маршалинг выполняется автоматически на системном уровне. Без него можно обойтись в случаях, когда к интерфейсу обращается другой процесс того же объекта (то есть объект и клиент принадлежат одному процессу — так называемый внутрипроцессный, или inproc-сервер, реализованный в виде DLL-библиотеки). Подробности будут приведены позже. При желании вы можете взять ответственность на себя и организовать «пользовательский маршалинг». Кроме того, необходимо обеспечить поддержку маршалинга во всех интерфейсах, которые вы определяете. Существуют специальные утилиты, которые берут упоминавшийся выше файл на языке IDL и генерируют по нему RPC-посредников и RPC-заглушки, содержащие стандартный код маршалинга и демаршалинга. В этой книге пользовательский маршалинг не рассматривается.
2.6 HRESULT и SCODE Выше я упоминал о том, что методы интерфейсов (в том числе и методы пользовательских интерфейсов COM и ActiveX) могут возвращать некоторое значение. По общепринятому соглашению методы интерфейсов возвращают значение типа HRESULT (за несколькими исключениями, среди которых IUnknown::AddRef и IUnknown::Release). Сокращение HRESULT означает «логический номер результата», однако в действительности это 32-разрядный код, состоящий из трех полей: собственно код статуса (16 бит), код компонента, в котором произошла ошибка, и код результата (успех или неудача). Все значения HRESULT, определяемые на системном уровне, находятся в заголовочном файле WINERROR.H. Одно время предполагалось, что HRESULT действительно будет логическим номером ошибки, а вся служебная информация будет храниться в специальной структуре SCODE. Когда в Windows NT 3.5 впервые появилась 32разрядная реализация OLE, Microsoft решила просто сохранить SCODE внутри HRESULT и сделать их синонимами. Это привело к многочисленным недоразумениям, и в наше время SCODE почти вымерли, остались только HRESULT. Если написано SCODE — следует читать HRESULT. ActiveX содержит ряд функций и макросов для работы с кодами HRESULT. Функция GetScode получает HRESULT и возвращает SCODE, на который он ссылается. ResultFromScode делает обратное (разумеется, с исчезновением различий между HRESULT и SCODE эти функции стали бесполезными; сейчас они считаются устаревшими). Макросы SUCCEEDED и FAILED проверяют значение HRESULT и определяют по нему, чем закончилась операция — успехом или неудачей. Макрос MAKE_HRESULT применяется для создания нестандартных кодов HRESULT. Компоненты, не являющиеся частью системы и задающие коды HRESULT, обычно содержат заголовочный файл с соответствующими определениями. Почти все HRESULT, с которыми нам
www.books-shop.com
предстоит иметь дело, определяются для элементов ActiveX; они находятся в заголовочном файле OLECTL.H.
2.7 Мой первый интерфейсный указатель Пора ответить на главный вопрос: так как же получить в свое распоряжение интерфейсный указатель? Ответ зависит от вашей рабочей среды. Например, если вы пишете на Visual Basic, выполните код следующего вида:
Dim MyIP As Object Set MyIP = CreateObject("MyApp.MyClass.1") За двумя строками программы спрятано довольно много интересного, к тому же они позволяют получить от объекта указатель только на конкретный интерфейс IDispatch (IDispatch представляет собой «стандартный интерфейс программного управления объектом», но мы пока не будем отвлекаться на подобные мелочи). Интерфейс IDispatch является ключевым для технологии Automation, которая исключительно важна для элементов ActiveX.
ЗАМЕЧАНИЕ Описанная ниже методика создания объектов называется «поздним связыванием» (смысл термина будет объяснен позже). В Visual Basic и в других языках с поддержкой Automation объекты можно создавать и при помощи «раннего связывания» (о нем тоже говорится позже). При раннем связывании действия будут несколько другими.
Давайте посмотрим, что же происходит в Visual Basic, COM-системе и в самом объекте при выполнении каждой строки кода. Разумеется, то же самое можно проделать и при получении интерфейсного указателя на C или C++: 1. 2.
(Visual Basic) Создать переменную типа Object с именем MyIP. (Visual Basic) Произвести поиск MyApp.MyClass.1 в категории реестра HKEY_ CLASSES_ROOT. Если элемент реестра не найден, процедура завершается неудачей, в противном случае из реестра извлекается значение CLSID. 3. (Visual Basic) Вызвать CoCreateInstance для полученного выше CLSID и IID интерфейса IDispatch ({00020400-0000-0000-C000-000000000046}, если это вас интересует!). 4. (COM) Вызвать CoGetClassObject для полученного выше CLSID и IID интерфейса IClassFactory. Если вызов заканчивается успешно, загружается выполняемый файл или DLL-библиотека с объектом. 5. (Объект) Вызвать CoRegisterClassObject и сообщить COM о том, что «фабрика класса» готова к работе. 6. (COM) Вызвать IClassFactory::CreateInstance для IID, переданного CoCreateInstance. Если вызов заканчивается успешно, создается указатель на интерфейс IDispatch данного объекта. 7. (COM) Вызвать IClassFactory::Release, чтобы освободить указатель на интерфейс IClassFactory данного объекта. 8. (Visual Basic) Сохранить полученный указатель на интерфейс IDispatch в переменной MyIP.
Функции CoCreateInstance и CoGetClassObject находятся в библиотеках COM (на что указывает префикс Co). Я пропустил некоторые второстепенные подробности, мы поговорим о них позже. А пока давайте выясним, что такое реестр и интерфейс IClassFactory.
2.8 Реестр «Реестром» называется системная база данных, используемая для самых различных целей — хранения информации о параметрах системы, о пользователе и, в частности, о конфигурации COM и ActiveX. Информация в реестре упорядочивается по разделам; один из них,
www.books-shop.com
HKEY_CLASSES_ROOT, содержит все интересующие нас сведения о COM и ActiveX. Этот раздел на самом деле представляет собой сокращенную запись для HKEY_LOCAL_MACHINE\SOFTWARE\ Classes. Чтобы COM-объектом можно было пользоваться, его необходимо предварительно зарегистрировать в реестре. Многие приложения (например, Microsoft Excel) делают это во время установки и проверяют наличие и правильность данных реестра при каждом последующем запуске. Как мы вскоре увидим, если COM-объект хранится в DLL-библиотеке (как элементы ActiveX), ситуация выглядит несколько иначе. Для каждого зарегистрированного класса в разделе HKEY_CLASSES_ROOT создается подраздел, где среди прочего хранится CLSID и имя выполняемого файла и/или DLL-библиотеки, в которой хранится объект (рис. 2-4). Подраздел CLSID раздела HKEY_CLASSES_ROOT содержит данные о всех CLSID в системе. Подраздел Interface содержит IID всех известных интерфейсов (поддерживаемых как COM, так и приложениями). Путь к записям реестра внешне напоминает файловые пути, так что CLSID объекта можно записать как HKEY_ CLASSES_ROOT\CLSID\{...} По мере знакомства с ActiveX вообще и элементами ActiveX в частности, мы более внимательно рассмотрим содержимое реестра.
Рис. 2-4. Часть иерархии реестра В приведенном выше примере Visual Basic просматривает реестр и находит там сведения о нашем объекте. Затем он извлекает значение CLSID и передает его функции COM API CoCreateInstance вместе с IID интерфейса, на который он желает получить указатель — IDispatch. Затем CoCreateInstance вызывает функцию CoGetClassObject, задача которой — найти DLL-библиотеку или выполняемый файл, содержащий объект, загрузить его и запросить интерфейс «объекта класса». Объект класса поддерживает интерфейс IClassFactory (или его разновидность) и может использоваться для создания экземпляров соответствующего объекта. На первый взгляд это выглядит довольно непонятно, поэтому рассматривайте это следующим образом: для каждого объекта, который может создаваться средствами COM, необходимо предоставить объект класса, по которому COM будет создавать экземпляры основного объекта. Функция CoGetClassObject получает интерфейсный указатель именно на этот объект класса. Когда библиотеки COM пытаются определить местонахождение выполняемого файла или DLLбиблиотеки, в которой хранится объект, они просматривают раздел HKEY_CLASSES_ROOT\CLSID\ {идентификатор-класса} реестра в поисках ключей LocalServer32, LocalServer, InprocServer32 и InprocServer. Ключи LocalServer указывают местонахождение выполняемых файлов, а InprocServer — DLL-библиотек. Наличие значения у ключа означает, что объект существует в данной форме. Например, если объект существует только в виде 32-разрядной DLL-библиотеки,
www.books-shop.com
значение будет иметь только ключ InprocServer32. Остальные ключи могут вообще отсутствовать в реестре (обычно именно так и бывает). Поскольку DLL-библиотека относится к тому же процессу, что и работающая с ней программа, общение между внутрипроцессным сервером (сервером, находящимся внутри процесса, — термин COM для объектов, реализованных в виде DLL-библиотек) и клиентом будет происходит гораздо быстрее, чем взаимодействие между локальным сервером и его клиентом, при котором каждый «сеанс связи» должен сопровождаться переключением контекста. Переключение контекста происходит, когда операционная система переключается с одной работающей задачи на другую, обычно эта операция происходит достаточно медленно. Соответственно, COM всегда сначала пытается найти DLL-версию объекта, и только если попытка окажется неудачной, идет поиск EXE-файла. В 32-разрядной среде (такой, как Windows 95) 16-разрядный клиент может общаться как с 16-, так и с 32-разрядным сервером; то же самое относится и к 32-разрядным клиентам. Разрядность другой стороны процесса взаимодействия несущественна, так что все эти подробности оказываются скрытыми. Следует учесть, что некоторые комбинации невозможны — например, Win32 не может загрузить 16-разрядную DLL-библиотеку в адресное пространство 32разрядного процесса, поэтому 16-разрядный внутрипроцессный сервер не может использоваться 32-разрядным клиентом. Тем не менее 16-разрядные клиенты могут успешно взаимодействовать с 32-разрядными внутрипроцессными серверами, но только не через интерфейс IDispatch.
2.9 IClassFactory IClassFactory — второй по значимости COM-интерфейс. Он служит «шлюзом» для доступа к интерфейсам класса. Когда вы вызываете CoGetClassObject, чтобы получить указатель на интерфейс IClassFactory данного объекта, то на самом деле полученный указатель относится вовсе не к объекту. Хотя код объекта к настоящему моменту (по всей вероятности) находится где-то в памяти, вы получаете указатель на интерфейс, единственное назначение которого — создать нечто, реализующее тот интерфейс, который вам действительно нужен. Во многих случаях это «нечто» представляет собой класс C++. Зачем нужны фабрики классов? Почему нельзя сразу получить указатель на экземпляр нужного объекта? Основная причина заключается в том, что фабрики классов определяют семантику создания объектов данного класса — некоторые из них пользуются одним экземпляром объекта для удовлетворения всех запросов клиентов на получение интерфейса, в других случаях при каждом запросе создается новый экземпляр объекта. В дополнение к методам IUnknown интерфейс IClassFactory содержит методы CreateInstance и LockServer:
class IClassFactory : public IUnknown {public: HRESULT CreateInstance(LPUNKNOWN pUnkOuter, REFIID iid, LPVOID *ppvObj); HRESULT LockServer(BOOL fLock); }; Метод CreateInstance в COM эквивалентен оператору new в C++ — он создает новый объект класса, к которому принадлежит фабрика класса, и возвращает указатель на требуемый интерфейс данного объекта. Первый параметр CreateInstance мы пока проигнорируем. Второй параметр — ссылка на требуемый IID, в моем сценарии с Visual Basic это IID интерфейса IDispatch. Третий параметр является указателем на область памяти, где должен храниться полученный интерфейсный указатель. Разумеется, вызов CreateInstance по ряду причин может закончиться неудачей — например, ошибка может произойти при создании нового объекта (скажем, из-за нехватки памяти), или же требуемый интерфейс не поддерживается объектом. Код ошибки передается в виде возвращаемого значения. Метод LockServer предназначен для оптимизации и ускорения динамического создания объектов данного класса. Он используется для хранения в памяти сервера, создающего соответствующие объекты, даже если в данный момент не существует ни одного экземпляра этого объекта. Благодаря этому системе не приходится загружать выполняемый файл или DLL-библиотеку при каждом создании нового объекта данного класса. Ранее в этой главе я говорил, что наличие
www.books-shop.com
интерфейса гарантирует лишь сам факт его реализации, а не ее конкретный вид. Например, в некоторых реализациях метод LockServer вообще не выполняет никаких действий. Но даже этот маловероятный случай вовсе не означает, что контракт интерфейса нарушен. Во-первых, метод существует. Во-вторых, он обладает правильной семантикой — теоретически он позволяет заблокировать сервер объекта в памяти. Неважно, что на самом деле он не выполняет никаких физических операций. Пользователь LockServer не сможет определить, блокируется ли сервер в памяти или нет — более того, для него это не имеет никакого значения. Следовательно, весь побочный эффект сводится к тому, что создание объектов будет занимать несколько больше времени, чем при «правильной» реализации LockServer.
IClassFactory2 В спецификации элементов ActiveX появилась новая разновидность интерфейса IClassFactory: IClassFactory2. Эти два интерфейса почти не отличаются, за исключением того, что IClassFactory2 поддерживает лицензирование (о лицензировании рассказано в главе 16). Появление IClassFactory2 наглядно демонстрирует истинную природу интерфейса как контракта. Разработчики ActiveX пришли к выводу, что для поддержки лицензирования необходимо наделить фабрики классов новыми возможностями. Поскольку интерфейс IClassFactory уже определен, его нельзя изменить, поэтому они создали новый интерфейс IClassFactory2.
2.10 Использование других объектов-включение В главе 1 мы рассмотрели преимущества объектно-ориентированного программирования и наследования — одного из самых больших достоинств ООП. Как было сказано, наследованием называется возможность изменения свойств данного класса посредством создания производных классов. Конечно, эти преимущества ориентированы скорее на разработчика, нежели на пользователя — последний вообще не интересуется, как написана программа, если она справляется со своими задачами. Наследование от runtime-объектов сопряжено с определенными сложностями, которых разработчики COM решили избежать. Одна из них — это часто упоминающаяся «проблема неустойчивости базовых классов». При наследовании интерфейсы базового класса должны оставаться неизменными или, по крайней мере, необходимо обеспечить совместимость новых интерфейсов со старыми. К сожалению, предсказать будущее невозможно (и одним из примеров может послужить добавление интерфейса IClassFactory2 в спецификацию элементов ActiveX). COM предлагает свое решение проблемы — включение посредством агрегирования. Это означает, что существующий объект целиком внедряется внутрь нового объекта. Для окружающего мира существует только один составной объект, обладающий теми интерфейсами, который он захочет поддерживать. Одни интерфейсы поддерживаются внешним объектом, другие — внутренним, а третьи — и тем и другим одновременно. Объект может агрегировать любое количество других объектов. В свою очередь, внутренний объект сам может быть внешним по отношению к какому-то другому объекту. Теперь давайте подумаем — если один объект находится внутри другого, то что произойдет при вызове метода QueryInterface для одного из интерфейсов объекта? Разумеется, если интерфейс принадлежит внешнему объекту, то вы получите указатель на интерфейс внешнего объекта, если же он принадлежит внутреннему объекту, то вы получите указатель на интерфейс внутреннего объекта. Подобная неоднозначность оказывается особенно неприятной, когда интерфейс может поддерживаться как внешним, так и внутренним объектом (например, оба объекта поддерживают интерфейс IDispatch). При объединении эта проблема решается просто: все интерфейсы внутреннего объекта, кроме IUnknown, должны делегировать (поручить) обработку всех трех методов IUnknown внешнему объекту. Внешний объект называется «управляющим объектом», а его реализация интерфейса IUnknown для составного объекта — «управляющим IUnknown». Это единственная реализация IUnknown, видимая за пределами данного объекта. Когда внешний объект создает экземпляр внутреннего объекта, он передает ему указатель на управляющий IUnknown. Именно для этой цели и служит первый параметр метода IClassFactory:: CreateInstance. Если этот параметр отличен от NULL, то объект создается как внутренний. Если он не допускает агрегирования, то вызов CreateInstance должен закончиться неудачей; в противном случае объект сохраняет указатель на управляющий IUnknown в своих внутренних данных и пользуется им для делегирования методов IUnknown всех интерфейсов (за исключением своей собственной реализации IUnknown).
www.books-shop.com
Управляемый объект обычно сохраняет указатель на управляющий IUnknown в локальной переменной. Если же он не участвует в агрегировании (то есть работает автономно), то в эту переменную заносится указатель на его собственный интерфейс IUnknown. Пользуясь локальным указателем, объект может работать с одним и тем же кодом независимо от того, агрегирован он или нет. Управляющий объект решает, какие интерфейсы внутреннего объекта должны быть открыты внешнему миру. Кроме того, он комбинирует свои интерфейсы с интерфейсами внутреннего объекта для того, чтобы объединенный объект обладал составными интерфейсами. В нашем обсуждении предполагалось, что один объект находится внутри другого. На самом деле любой объект может агрегировать любое количество других объектов, которые, в свою очередь, также могут агрегировать другие объекты. Тем не менее каждый объект может сам решать, желает он агрегироваться или нет.
2.11 Automation и IDispatch Ранее я уже упоминал об Automation. Напомню, что под этим термином понимается возможность программировать поведение внешнего объекта средствами ActiveX. Технология Automation была разработана в первую очередь для языков высокого уровня (например, Visual Basic) и макроязыков приложений (что для Microsoft также означает Visual Basic и его разновидности!), хотя ей можно с таким же успехом пользоваться и в программах на Java, C и C++. Клиенты Automation называются «контроллерами». На базе Automation построена вся работа элементов ActiveX, поэтому мы должны рассмотреть ее достаточно подробно. В этой главе Automation уделено столько же внимания, как и всем остальным аспектам ActiveX и COM, однако в следующей главе эта тема рассматривается значительно глубже и даже приводятся примеры программ. Вообще говоря, если вас интересуют лишь азы Automation, можно обойтись и без чтения главы 3, однако для тех, кто захочет разобраться в принципах ее работы, эта глава окажется весьма познавательной.
2.12 Свойства, методы и события Для работы Automation необходимо, чтобы поведение объекта описывалось в виде набора свойств, методов и событий. Мы будем пользоваться следующими рабочими определениями:
«Свойствами» называются атрибуты объекта — например, цвет, почтовый индекс или ссылка на другой объект. «Методом» в Automation называется запрос на выполнение объектом некоторого действия. При помощи «событий» объект сообщает пользователю о выполнении некоторого условия. События похожи на методы, за исключением того, что они посылаются в противоположном направлении — от объекта к пользователю. В этой главе мы не станем останавливаться на событиях, поскольку механизм их работы подробно описан в следующей главе. Тем не менее следует обратить внимание на особое место событий в семействе Automation — пользователи объекта получают информацию о них из библиотек типов, а для вызова событий обычно используется механизм Automation.
Разумеется, свойства имеют типы — так, почтовый индекс может быть представлен текстовой строкой или длинным целым, в зависимости от того, в какой стране вы живете. Свойства могут быть параметризованы — например, это может пригодиться при работе с массивом однотипных элементов, представляющим набор свойств объекта (скажем, строки почтового адреса). Для такого свойства можно задать параметр, равный индексу массива для читаемой или записываемой величины. Методы также могут получать параметры и возвращать значения. Средства Automation позволяют организовать иерархическую структуру объектов — методы и свойства могут возвращать указатели на другие объекты (точнее, указатели на интерфейсы других объектов). Представьте себе объект «клиент», одним из атрибутов которого является адрес клиента. Вместо того, чтобы представлять каждый элемент адреса в виде отдельного свойства, объект «клиент» представляет весь адрес как объект, программируемый средствами Automation. Этот объект, в свою очередь, содержит свойства для каждой строки адреса, почтового индекса, области и т. д. Более того, поскольку адрес является самостоятельным объектом, при помощи методов можно потребовать от него выполнения каких-либо особых
www.books-shop.com
действий — например, проверки индекса. Готовым объектом «адрес» можно будет воспользоваться в другом месте. Automation можно рассматривать как стандартный механизм, при помощи которого объект может задать свои свойства, методы и типы, а также предоставить к ним доступ. Во многих контроллерах Automation используется стандартный интерфейс IDispatch, который стал первым механизмом такого рода и был увековечен в таких языках высокого уровня, как Visual Basic. Тем не менее Automation также поддерживает концепцию «двойственного интерфейса», в который входят как методы IDispatch, так и методы обычных интерфейсов. Обе возможности рассмотрены в последующих разделах этой главы. Для Automation исключительно важен способ, при помощи которого объект сообщает информацию о себе — так называемые «библиотеки типов». О библиотеках типов, исключительно важных для элементов ActiveX, также рассказано в следующем разделе.
2.13 Automation на основе IDispatch Как я уже говорил, объект раскрывает внешнему миру свои свойства и методы через IDispatch, специальный интерфейс Automation. IDispatch не представляет методы объектов как свои собственные (и это вполне логично, поскольку интерфейс должен быть неизменным). Вместо этого он позволяет работать с методами и свойствами объекта через свой метод Invoke.Помимо методов IUnknown, IDispatch содержит еще четыре метода: GetIDsOfNames, GetTypeInfo, GetTypeInfoCount и Invoke. В рассмотренном ранее примере на Visual Basic значение свойства Automation можно получить следующим образом:
MsgBox MyIP.SomeProperty В этом фрагменте мы просим у объекта, указатель на интерфейс IDispatch которого находится в переменной MyIP, вернуть значение свойства с именем SomeProperty. Visual Basic выводит нужное значение в окне сообщения. Вот что при этом происходит: 1. 2. 3.
(Visual Basic) Вызвать метод MyIP::GetIDsOfNames для определения dispid (идентификатора диспетчеризации) свойства SomeProperty. (Visual Basic) Вызвать метод MyIP::Invoke для найденного dispid свойства SomeProperty. (Visual Basic) Если вызов метода завершился успешно, вывести найденное значение; в противном случае Visual Basic инициирует ошибку, тип которой зависит от исключения, возвращенного при вызове Invoke.
Именно функция IDispatch::GetIDsOfNames обеспечивает возможность позднего связывания в Automation на основе IDispatch. Позднее мы увидим, что информация для раннего связывания хранится в библиотеках типов. При «позднем связывании» контроллер Automation определяет, существует ли требуемое свойство и метод, непосредственно во время выполнения программы, тогда как при «раннем связывании» правильность вызова проверяется во время компиляции. У обоих видов связывания имеются свои преимущества: раннее связывание отличается большей надежностью и обычно быстрее работает, поскольку имена свойств и методов, параметры и т. д. проверяются заранее, а при позднем связывании неизвестное свойство, метод или неверный параметр могут привести к runtime-ошибкам. С другой стороны, при позднем связывании программа может динамически определить возможности объекта — благодаря этому обстоятельству удается написать программу для работы с конкретными объектами, а потом добавить к ним новые, неизвестные на момент написания кода. В Visual Basic 3.0 предусмотрено только позднее связывание, а последующие версии поддерживают работу с библиотеками типов и, следовательно, раннее связывание. Вскоре мы рассмотрим Automation на основе двойственных интерфейсов. С ее помощью можно реализовать еще одну разновидность связывания (связывание через v-таблицу). Идентификаторы диспетчеризации (dispid) Хотя при программировании на языке высокого уровня может показаться, что на методы и свойства объектов Automation можно ссылаться по именам, на самом деле при работе через IDispatch используются специальные числовые идентификаторы (идентификаторы диспетчеризации, или dispid) свойств и методов. Параметры тоже могут быть именованными, однако при их передаче контроллер Automation сначала преобразует имена в числовые значения dispid. Преобразованием символических имен во внутренние значения dispid, присвоенные этим
www.books-shop.com
именам создателем объекта Automation, занимается метод IDispatch с именем GetIDsOfNames. Каждый вызов GetIDsOfNames может преобразовать в dispid одно имя свойства или метода вместе с произвольным количеством имен параметров данного свойства или метода. GetIDsOfNames заполняет массив, передаваемый по ссылке, значениями dispid всех переданных имен. Если хотя бы одно из имен не будет опознано объектом Automation, то метод возвращает значение DISP_E_UNKNOWNNAME, а во все элементы массива, соответствующие неизвестным именам, вместо значения dispid заносится константа DISPID_UNKNOWN (–1). Для чего нужны именованные параметры? Если все параметры метода или свойства являются обязательными, то можно обойтись и без присвоения имен — это всего лишь вопрос удобства. Тем не менее некоторые объекты Automation могут получать любое количество параметров из заданного набора, следующих в произвольном порядке. Такие объекты либо игнорируют отсутствующие параметры, либо присваивают им значения по умолчанию. Для этого в Automation предусмотрена возможность присваивать параметрам значение по имени, в отличие от традиционной схемы присвоения значений по номеру. В следующем вызове метода MyMethod использована стандартная схема, при которой все параметры являются обязательными, а порядок их следования определен внутри метода:
If MyIP.MyMethod (1, "Hello") = False Then … Контроллер Automation знает, что первый параметр стоит первым в списке (в нашем примере 1), а второй следует за ним (в нашем примере «Hello»). Следовательно, ему не нужно получать значения dispid параметров, поскольку он передаст их в метод IDispatch::Invoke по номеру. Но давайте рассмотрим другой пример (в синтаксисе Visual Basic):
If MyIP.MyOtherMethod (String := "Hello") = False Then … В этом случае уже нельзя быть уверенным, что метод MyOtherMethod имеет всего один параметр, и контроллер Automation не знает, в каком месте списка параметров MyOtherMethod будет искать параметр String. Следовательно, он должен получить dispid параметра String и передать его методу Invoke. После того как имя свойства или метода вместе с именами параметров будет преобразовано в dispid, метод IDispatch::Invoke используется для обращения к объекту Automation. Обратите внимание на то, что свойства (как и методы) реализуются посредством вызова функций. Функции для работы со свойствами содержат внутренние пометки, отличающие их от методов, — обычно на каждое свойство приходится по две такие функции (одна возвращает значение свойства, а другая его присваивает). Некоторые контроллеры Automation вообще не отличают свойства от методов. Метод IDispatch::Invoke вызывает функции свойств и методов объекта Automation. Среди получаемых им параметров наиболее важными являются следующие:
dispid вызываемого метода или свойства. Указатель на массив аргументов метода или свойства. Указатель на область памяти, где должен храниться результат (если он есть). Указатель на структуру EXCEPINFO.
С последним параметром связан один интересный момент. Если метод Invoke возвращает DISP_E_EXCEPTION, значит, в вызванной функции или в ActiveX возникло исключение. В Automation «исключением» называется сообщение от вызванной функции или ActiveX, которое указывает на возникновение какой-то проблемы. Ситуация может быть как тривиальной (неверный тип аргумента, выход за пределы допустимых значений), так и достаточно сложной (сообщение о сетевой ошибке). В структуре EXCEPINFO пользователь найдет самые разнообразные сведения об исключении. Здесь имеется содержательная строка с описанием ситуации (учтите, что описание исключения, возникшего внутри объекта, предоставляется самим объектом — это единственный разумный источник информации); имя справочного файла и контекстный идентификатор внутри него, чтобы вызывающее приложение могло вывести справку по ошибке; и, разумеется, код ошибки или HRESULT, на основании которого работают операторы типа On Error Goto в Visual Basic. В первоначальном варианте технология Automation была ориентирована на сценарные языки наподобие Visual Basic, поэтому содержащаяся в структуре EXCEPINFO информация исключительно важна. Это особенно справедливо по отношению к объектам позднего связывания, так как работа этой схемы основана на возникновении runtimeошибок от объекта или ActiveX. Ⱦɚɧɧɚɹɜɟɪɫɢɹɤɧɢɝɢɜɵɩɭɳɟɧɚɷɥɟɤɬɪɨɧɧɵɦɢɡɞɚɬɟɥɶɫɬɜɨɦ%RRNVVKRS ɊɚɫɩɪɨɫɬɪɚɧɟɧɢɟɩɪɨɞɚɠɚɩɟɪɟɡɚɩɢɫɶɞɚɧɧɨɣɤɧɢɝɢɢɥɢɟɟɱɚɫɬɟɣɁȺɉɊȿɓȿɇɕ Ɉɜɫɟɯɧɚɪɭɲɟɧɢɹɯɩɪɨɫɶɛɚɫɨɨɛɳɚɬɶɩɨɚɞɪɟɫɭ[email protected]
Массив параметров, передаваемых Invoke, на самом деле представляет собой массив структур VARIANT. Находящиеся в них объединения (union в терминах C) способны хранить любой тип данных, которые IDispatch может опознать (и, следовательно, выполнить маршалинг). Структура также включает поле типа, по которому можно определить тип хранящихся в ней данных. Роль структур VARIANT не ограничивается передачей методу Invoke массива с однородными элементами, удобными для анализа, — они позволяют создавать методы и свойства с переменным типом параметров.
2.14 Automation на основе двойственных интерфейсов Интерфейс IDispatch обладает достаточно гибкими и удобными средствами для работы со свойствами и методами объектов. Он идеально подходит для таких языков высокого уровня, как Visual Basic, однако за гибкость и простоту приходится расплачиваться скоростью. Конечно, обращение к методу объекта через IDispatch::Invoke происходит медленнее, чем прямой вызов этого метода. Почему? Прежде всего, он требует лишнего вызова функции и неэффективной передачи параметров и возвращаемых значений в структурах VARIANT, когда вам всего лишь требуется передать целое число. Лишний вызов функции приводит к самым значительным расходам в тех случаях, когда управляемый объект принадлежит к одному процессу с управляющим приложением (то есть для внутрипроцессных объектов Automation). Реализации IDispatch::Invoke обычно выглядят довольно сложно. Потребность в более гибких средствах поддержки Automation привела к появлению «двойственных интерфейсов». В этом случае объект предоставляет интерфейс типа обычного IDispatch тем контроллерам Automation, для которых это действительно необходимо. Другие контроллеры Automation работают с нестандартными интерфейсами (при условии, что они умеют с ними работать). Такие нестандартные интерфейсы является производными от IDispatch и обладают набором функций, работающих в обход методов IDispatch и непосредственно вызываемых контроллером. Эти дополнительные функции предназначены для работы со свойствами и методами объекта. Оба интерфейсных механизма описываются одними и теми же библиотеками типов, поэтому двойственные интерфейсы позволяют осуществлять оба вида связывания (раннее и позднее) как с использованием IDispatch, так и без него. Это означает, что методы двойственного интерфейса могут получать и возвращать данные лишь тех типов, которые известны Automation. Процесс связывания в двойственном интерфейсе часто называется «связыванием через v-таблицу», поскольку при нем вызывающая функция непосредственно работает с v-таблицей объекта. Хотя реализация двойственного интерфейса ни в коем случае не навязывает такого подхода, в большинстве случаев вызовы методов и свойств через IDispatch перенаправляются в «нормальные» методы, работающие с v-таблицей. На рис. 2-5 изображены основные положения типичного двойственного интерфейса. Метод IDispatch::Invoke обладает специальной семантикой для обработки ошибок, возвращаемых вызванными методами и свойствами, — речь идет об описанной выше структуре EXCEPINFO. Поскольку методы и свойства, вызываемые через двойственный интерфейс, не обязаны пользоваться механизмом IDispatch::Invoke, был предусмотрен новый механизм обработки ошибок. Он предоставляет ту же информацию, что и структура EXCEPINFO в Invoke, однако контроллер Automation должен вызывать его в том случае, если в ходе обращения к методу или свойству было обнаружено исключение (более подробная информация приведена в разделе «Исключения и двойственные интерфейсы» главы 9).
www.books-shop.com
Рис. 2-5. Типичный двойственный интерфейс
2.15 Библиотеки типов Ранее я уже упоминал библиотеки типов и информацию о типах. «Библиотеки типов» сообщают окружающему миру о свойствах и методах (а также событиях) объекта, о типах возвращаемых значений и параметрах, об используемых dispid и даже о том, в каком файле и разделе справочной системы хранится информация про объект, его методы и свойства. Все двойственные интерфейсы должны быть описаны в библиотеках типов. Когда-то библиотеки типов создавались в виде текстовых файлов на «языке описания объектов» (ODL), компилируемых утилитой MkTypeLib. Этот способ может применяться и сейчас, однако Microsoft постепенно начинает отделять ODL от другого языка, используемого в COM, — MIDL (язык описания интерфейсов Microsoft). Компилятор MIDL, входящий в поставку Win32 SDK для Windows NT 4.0 и последующих версий, может компилировать файлы на ODL и создавать библиотеки типов (в более ранних версиях MIDL это было невозможно). Со временем утилита MkTypeLib исчезнет. Библиотеки типов используются контроллерами Automation, которые должны поддерживать раннее связывание, и программными средствами для получения информации об объектах (например, программа просмотра объектов в Microsoft Excel). Хорошим примером использования библиотек типов для раннего связывания является часть реализации ClassWizard из Microsoft Visual C++, благодаря которой можно взять библиотеку типов и создать по ней определения классов для «объектов-посредников» (не путать с RPC-посредниками), которые представляют описываемые библиотекой интерфейсы Automation. Например, если создать библиотеку типов для нескольких объектов Automation и прочитать ее при помощи ClassWizard, то вы получите файл на языке Visual C++, содержащий определение класса для каждого объекта в библиотеке, а через функции этих классов можно будет обращаться к методам и свойствам объектов. Остается лишь выполнить действия, необходимые для установления связи между экземпляром класса и экземпляром реального объекта. В дальнейшем операции с таким классом в программе на Visual C++ будут вызывать аналогичные операции с объектом на реальном сервере, отсюда и термин — «объект-посредник». Другой пример можно найти в Visual Basic. Если у объекта имеется библиотека типов, то программа на Visual Basic может воспользоваться ею и осуществить раннее связывание. В стандартном случае (с поздним связыванием) применяется оператор Dim x As Object:
Dim x As Object Set x = CreateObject("MyObject.1") x.SomeProperty = 3 Этот фрагмент обладает как минимум двумя недостатками и одним очевидным достоинством. Первый недостаток: во время компиляции невозможно определить, имеет ли объект «MyObject.1» свойство с именем SomeProperty и позволяет ли тип этого свойства присвоить ему значение 3. Если объект не имеет такого свойства, происходит runtime-ошибка. Второй недостаток: сгенерированный код должен определить значение dispid для свойства SomeProperty (при помощи вызова IDispatch::GetIDsOfNames), а эта операция занимает много времени. Единственное преимущество состоит в том, что строка «MyObject.1» может быть динамической — это позволяет программе работать с различными и даже неизвестными объектами.
www.books-shop.com
Visual Basic 4.0 и более поздних версий способен работать с библиотеками типов, поэтому приведенный выше фрагмент можно усовершенствовать при условии, что для объекта существует библиотека типов, в которой его интерфейс диспетчеризации называется CMyInterface:
Dim x As New CMyInterface x.SomeProperty = 3 Первое отличие заключается в том, что благодаря явному объявлению переменной типа CMyInterface компилятор сможет выполнить проверку типа для SomeProperty. Ключевое слово New указывает Visual Basic, что объект указанного типа должен быть создан (посредством CoCreateInstance, как было описано выше), когда в нем возникнет необходимость — это происходит в следующей строке, где определяется значение свойства SomeProperty. Наконец, двойственность интерфейса CMyInterface обеспечивает выигрыш в скорости, поскольку Visual Basic осуществляет связывание через v-таблицу. Библиотеки типов чрезвычайно важны для Automation, потому что приложения могут найти в них описание интерфейсов тех объектов, с которыми они работают. Даже фактическая реализация механизма для работы с этими объектами (IDispatch или двойственные интерфейсы) уступает им по значимости. Сведения о библиотеках типов обычно заносятся в реестр (как и сведения об объектах ActiveX). Подраздел TypeLib в разделе HKEY_CLASSES_ROOT содержит подразделы, в которых хранятся значения GUID для всех библиотек типов. Объекты, для которых создаются библиотеки типов, обычно сами регистрируют их и включают соответствующую ссылку в свою запись реестра. Например, запись реестра для CLSID объекта AutoProg (см. следующую главу) может ссылаться на свою библиотеку типов следующим образом:
HKEY_CLASSES_ROOT\CLSID\{AEE97356-B614-11CD-92B408002B291EED}\TypeLib = {4D9FFA38-B732-11CD-92B4-08002B291EED} В библиотеках типов можно почерпнуть значительно больше сведений, чем описано в этом разделе. С некоторыми из них нам предстоит столкнуться при разработке элементов ActiveX в этой книге. Другие сведения из библиотек типов не имеют прямого отношения к элементам ActiveX. Более подробную информацию о них можно найти в разделах ActiveX SDK (на прилагаемом к книге диске CD-ROM), относящихся к библиотекам типов, Automation и MIDL. Для работы с библиотеками типов существуют специальные COM-интерфейсы. В частности, интерфейсы ITypeLib и ITypeInfo применяются для поиска информации, а ICreateTypeLib и ICreateTypeInfo — для создания библиотек. Два последних интерфейса используются утилитами MIDL и MkTypeLib для преобразования файлов на языках IDL и ODL в библиотеки типов.
2.16 GetTypeInfoCount и GetTypeInfo Возможно, вы еще не забыли, что рассмотрение двух методов IDispatch было отложено до знакомства с библиотеками типов. Теперь мы можем поговорить об этих методах — GetTypeInfoCount и GetTypeInfo. Метод GetTypeInfoCount возвращает количество описаний типов для интерфейса IDispatch, а GetTypeInfo — указатель на ITypeInfo для заданного номера описания типа. После появления Automation на основе двойственных интерфейсов языки IDL/ODL были доработаны, и в них появились новые ключевые слова. Более подробную информацию о них можно найти в разделе Automation справочной системы пакета Win32 SDK для Windows NT 3.51, Windows 95 или более поздних версий, или же справочной системе Visual C++ версии 4.2 и выше. Некоторые из этих ключевых слов встречаются в примерах настоящей книги. Знакомство с Automation можно начать с примеров, приведенных в следующей главе. Кроме того, если у вас имеется Visual Basic версии 4.0 и выше, почитайте документацию по созданию OLEсерверов и попробуйте создать несложное приложение. Управлять им можно из другой программы на Visual Basic или любого другого контроллера Automation — скажем, программы на Visual C++ или какого-нибудь приложения Microsoft Office.
2.17 Структурированное хранение
www.books-shop.com
В жизни любого пользователя неизбежно наступает момент, когда он прекращает работу, выключает компьютер и уходит домой. На следующий день он возвращается и снова включает компьютер. Вполне возможно, что ему захочется продолжить работу с того самого места, на котором он остановился вечером — в общем, вполне разумное желание. Если в выполняемой им работе участвуют компоненты (а в наши дни их роль постоянно растет), то как они могут запомнить свое состояние? Объект называется «устойчивым» (persistent), если он может существовать при выключенном компьютере. Работа устойчивых объектов может быть успешно продолжена в новом сеансе. Пусть наше описание устойчивости не совсем точно — разумеется, при выключенном питании никакая программа выполняться не может. Если объект способен запомнить достаточно информации о своем состоянии (то есть данных), чтобы позднее по ней можно было создать другой объект с точно такими же характеристиками, то его можно считать устойчивым. Устойчивость — не вечная жизнь, а череда смертей и воскрешений! Сохранение состояния объекта на физическом носителе часто называют «сериализацией» (serialization), поскольку внутреннее представление объекта переносится на носитель в виде нескольких цепочек (серий) байтов. Такой подход помогает сохранить структуру данных объекта в неструктурированном файле. Любой файл MS-DOS или Win32 на самом деле является неструктурированным, его структурная интерпретация должна выполняться приложением. Не зная файлового формата приложения, вам будет непросто разобраться с его файлами. Именно по этой причине нередко возникают трудности с написанием программ для просмотра файлов (а также с обеспечением совместимости с конкурирующими программными продуктами!). С другой стороны, разработчику приложения тоже приходится нелегко, поскольку вставка или удаление блоков данных в неструктурированных файлах требует дополнительной работы. К тому же неструктурированные файлы затрудняют выполнение запросов к данным. Наилучший выход из положения предлагает объектно-ориентированная файловая система. Возможно, она появится в будущих версиях Windows NT, а пока приходится искать решение для существующих файловых систем. Компонентная объектная модель (COM) предлагает воспользоваться средствами «структурированного хранения», которые позволяют рассматривать стандартный файл как отдельную файловую систему, в которой имеются свои аналоги для каталогов и файлов. В ActiveX такие псевдо-каталоги называются «хранилищами» (storages), а псевдофайлы — «потоками» (streams). Хранилища и потоки (как и обычные каталоги и файлы) обладают именами, и для них даже можно задавать индивидуальные права доступа — подобная возможность отсутствовала в файловой системе MS-DOS (но появилась в Win32). Работа с хранилищами и потоками осуществляется через интерфейсы IStorage и IStream. Устойчивый объект должен реализовать интерфейс IPersistStorage или же воспользоваться реализованным в ActiveX интерфейсом IPersistStream, если его потребности не выходят за рамки простейших. Реализация ILockBytes (одного из интерфейсов, лежащих в основе структурного хранения) также позволяет хранить и извлекать данные не только из файлов, но и из других источников. Большая часть ответственности за открытие, ведение и закрытие хранилищ и потоков возлагается на приложение, которое пользуется объектами (а не на сами объекты). Можно рассмотреть следующий пример: если документ Microsoft Word содержит внедренную диаграмму Microsoft Excel, то при сохранении данных Word должен позаботиться о том, чтобы диаграмма Excel была сохранена в том же самом файле. Для этого он открывает файл средствами структурированного хранения, записывает в него комбинацию хранилищ и потоков по своему усмотрению, затем передает IStorage приложению Excel и просит сохранить объект в этом хранилище. Все, что требуется от Excel — создать необходимые хранилища нижнего уровня и сохранить свои данные в потоках, находящихся в них. Поскольку описанная ситуация встречается очень часто, OLE содержит несколько функций для автоматизации большей части этого процесса. Например, приложение может сохранить внедренные в него объекты функцией OleSave. OleSave получает указатель на интерфейс IPersistStream объекта; указатель на хранилище IStorage, в котором должен быть сохранен объект; и флаг, который определяет, совпадает ли это хранилище с тем, из которого был загружен объект. При выполнении подобных операций средствами структурированного хранения фактически сохраняется документ, состоящий из нескольких частей, отсюда возник термин «составной файл». Структурированное хранение не сводится к механизму для интерпретации отдельных файлов в виде файловой системы. Указатели на интерфейсы IStream и IStorage могут передаваться от
www.books-shop.com
процесса к процессу (с выполнением маршалинга), с их помощью можно организовать обмен данными между приложениями. Но даже это еще не все…
2.18 Структурированное хранение и отложенная запись Одно из ключевых достоинств модели структурного хранения на базе COM заключается в том, что она позволяет организовать так называемую «отложенную запись». Это означает, что изменения, внесенные в хранилище или поток, не переносятся немедленно на физический носитель, и при необходимости их можно отменить. Вы можете наделить подобной возможностью любое хранилище и тем самым по своему усмотрению производить и отменять совершаемые с ним операции. Чтобы закрепить внесенные изменения, необходимо вызвать метод IStorage::Commit, для их отмены вызывается метод IStorage::Revert. Отложенная запись в иерархии вложенных хранилищ вызывает ряд интересных проблем. При закреплении изменений во внутреннем хранилище физическая запись на диск не происходит до того момента, когда изменения будут закреплены и во внешнем хранилище. Если этого не произойдет, теряются даже закрепленные изменения во внутренних хранилищах. Ничто не мешает вам открыть одни хранилища иерархии в режиме отложенной записи, а другие — в непосредственном режиме. Непосредственный режим означает, что все изменения сразу же закрепляются, и отменить их уже не удастся! Кстати говоря, хотя интерфейс IStream тоже содержит методы Commit и Revert, текущие реализации OLE не поддерживают отложенную запись в потоках.
2.19 Структурированное хранение и элементы ActiveX Потребности элементов ActiveX в структурированном хранении обычно невелики.Чаще всего требуется сделать устойчивыми лишь свойства элемента, и то не все, а лишь те, которые должны сохраняться между сеансами работы. Следовательно, элементы могут реализовать интерфейс IPersistStorage, чтобы использующее их приложение могло приказать элементу сохранить значения его свойств. Раз элементы ActiveX пользуются средствами структурного хранения только для обеспечения устойчивости свойств, они не нуждаются в дополнительных возможностях хранилищ и вполне обходятся сохранением в потоках. Тем не менее интерфейса IPersistStream иногда не хватает для потребностей элемента. В таких случаях элементу следует реализовать новый интерфейс IPersistStreamInit. При наличии этого интерфейса управляющее элементом приложение должно работать именно с ним. В данный момент нет особого смысла объяснять, почему дело обстоит именно так, а не иначе, поэтому рассмотрение IPersistStreamInit и необходимость его использования элементами ActiveX откладывается до главы 3. Многие контейнеры позволяют сохранить сценарий элемента, часто включающий значения свойств, в доступной текстовой форме. Элементы могут реализовать интерфейс IPersistPropertyBag, который позволяет контейнеру, обладающему такой возможностью, оптимизировать процесс сериализации для текстового представления. Более подробная информация также приводится в следующей главе. С пришествием World Wide Web и Internet возникла неприятная проблема — на то, чтобы получить значения свойств, может уходить некоторое время. Раньше контейнер, работавший с типичными элементами, в худшем случае мог сохранить их свойства в файле на сетевом диске. Теперь нельзя исключить возможности, что значения свойств, сохраненные контейнером, окажутся в другом месте Сети. Хотя Internet-технологии развиваются чрезвычайно быстро, многие пользователи таких элементов и контейнеров общаются с Сетью с помощью модема со скоростью 9600, 14 400 или (если повезет) 28 800 бод. Если свойства относительно невелики (как, скажем, числа или строки), то особых трудностей не возникает, однако для свойств большого объема (растровых изображений, звуковых файлов и т. д.) ситуация оказывается совершенно иной. Допустим, вы написали элемент, в котором не учтены возможные проблемы доступа, и кто-то установил его в своей Web-странице. Если ваш элемент не будет реагировать на ввод информации пользователем до того момента, когда будут перекачаны значения всех свойств, он заработает скверную репутацию. Чтобы справиться с этой проблемой и обеспечить пользователям оптимальную производительность, которая достигается на линиях со скоростью 28 800 бод, Microsoft предусмотрела так называемые асинхронные, или «путевые» (datapath), свойства. Асинхронные свойства не пересылаются вместе со всеми остальными свойствами элемента, вместо этого их пересылка ведется в асинхронном режиме. Термин «путевые свойства» обусловлен тем, что значения таких свойств обычно хранятся по заданным URL, которые представляют собой путь к данным. При таком подходе элемент сможет реагировать на действия
www.books-shop.com
пользователя, как только он сам (если его также приходится пересылать) и его «простые» свойства окажутся на компьютере пользователя. Другие свойства прибудут позднее. При наличии нескольких асинхронных свойств (или если Web-страница содержит несколько элементов с такими свойствами) контейнер может создать отдельный программный поток (thread) для каждого асинхронного значения. Разумеется, любые действия, основанные на значениях асинхронных свойств, могут завершиться лишь после их получения. Вполне вероятно, что в будущем подобное поведение будет распространено и на элементы. Некоторые фрагменты кода элемента (скажем, обработчики редких событий) будут иметь более низкий приоритет по сравнению с остальными. Разумно написанный контейнер начнет с приема высокоприоритетных частей, а элемент сможет реагировать на отдельные виды пользовательского ввода еще до того, как будет принят весь код элемента! Выбор механизма, используемого для асинхронной пересылки свойств, зависит от «асинхронного моникера», также называемого «URL-моникером». О моникерах кратко рассказано в конце этой главы, а затем подробно — в следующей.
2.20 Создание сложных документов средствами ActiveX Большинство пользователей считает одним из главных преимуществ OLE возможность внедрять объекты, созданные в одном приложении, в документы, созданные другими приложениями. Классический пример был приведен в предыдущем разделе — документ Microsoft Word, содержащий диаграмму Microsoft Excel. Почему внедрение объектов так важно для пользователей? Потому что оно позволяет создавать сложные документы, содержащие данные в разных форматах и взятые из разных источников. Если бы приложения, создающие эту разнородную информацию, позволяли бы еще и редактировать ее похожими способами, то пользователи также выиграли бы от более тесной интеграции приложений (даже разработанных разными фирмами). В идеале пользователь вообще может не знать о том, какое приложение использовалось при создании той или иной части документа. OLE и ActiveX помогают добиться этой цели.
2.21 Визуальное редактирование Та часть спецификации ActiveX, которая занимается внедрением, связыванием и редактированием объектов, обычно называется «Документами ActiveX» (ранее — «Документы OLE») и содержит несколько внутренних категорий, относящихся к конкретным возможностям для работы с документами. Например, после внедрения диаграммы Excel в документ Word вы можете сделать на ней двойной щелчок и редактировать ее прямо внутри Word. В предыдущих версиях OLE дело обстояло иначе — запускалась копия Excel, в нее копировалась диаграмма, а пользователь страдал от сознания того, что ему приходится работать с двумя разными приложениями. ActiveX позволяет организовать совместную работу двух приложений, чтобы пользователь мог редактировать диаграмму на месте. Панели инструментов и команды меню приложения-создателя (Excel) объединяются с соответствующими средствами приложенияхранителя (Word). Это позволяет работать с инструментами для редактирования объекта вместо стандартных инструментов приложения-хранителя. Приложения могут обсудить все тонкости совместной работы, поэтому не исключено, что почти все меню и панели инструментов изменятся в соответствии со спецификой приложения-создателя. Возможна и другая ситуация — скажем, изменения сведутся к появлению пары новых команд в меню. Возможность редактирования внедренного объекта на месте называется «визуальным редактированием» (хотя в прошлом употреблялись и другие термины, а я полагаю, что его следовало бы назвать «Редактированием ActiveX»). В настоящее время Документы ActiveX бесспорно являются самой сложной частью всей технологии ActiveX, хотя это всего лишь малая часть общей картины ActiveX и OLE. Именно эта часть OLE когда-то смущала программистов и создавала обманчивое впечатление, что программировать для OLE исключительно сложно. В следующей главе я покажу, что вы вполне можете написать приложение ActiveX, которое ничего не знает о Документах ActiveX и потому выглядит очень просто. Тем не менее нам все же придется рассмотреть Документы ActiveX, потому что многие из их возможностей используются в элементах ActiveX. К тому же в 1995 году их спецификация была расширена для достижения большей гибкости — проблема исходных приложений OLE 2.0 с поддержкой визуального редактирования заключалась в том, что при печати, определении параметров страниц и т. д. внедренные или связанные объекты оставались
www.books-shop.com
в роли пассивных наблюдателей. В сущности, когда речь заходила о работе с документом в целом, внедренный объект мало чем отличался от обычной растровой картинки. В августе 1995 года вышел пакет Microsoft Office 95, и среди его приложений впервые появился Office Binder. Это приложение стало первым воплощением технологии Документов ActiveX (впрочем, тогда они назывались Doc- объектами). Binder позволял работать с подшивкой (то есть несколькими взаимосвязанными документами) как с одним документом. В подшивку могли входить документы, которые создавались в любых приложениях, поддерживающих интерфейс Doc-объектов — на тот момент такими приложениями были Word, Excel, PowerPoint и другие приложения Office. Зачем нужна подшивка? Ее можно послать по электронной почте как единое целое, можно проверить орфографию сразу во всех документах и даже применить ко всем документам некоторые атрибуты формата. Еще важнее, что Binder позволял напечатать сразу все объединяемые документы. Вам уже не приходилось по отдельности запускать Word, Excel и т. д. для печати соответствующей порции документов. Несомненно, все эти возможности были полезны для опытных пользователей Office. Впрочем, одна из возможностей Binder представляет для нас еще больший интерес: при открытии документа, входящего в подшивку, в окне Binder запускалось приложение-«владелец». Отчасти это напоминало визуальное редактирование, но с несколькими дополнениями. Во-первых, документ открывался целиком (в отличие от объектов, редактируемых на месте), но при этом оставался в окне Binder. Во-вторых, объединение меню в Binder обладало некоторыми возможностями, которых не было в ранние дни визуального редактирования. Например, меню Help теперь содержало две подкоманды; одна выводила справку по работе Binder, а другая — по работе приложения-«владельца». Кроме того, в Binder появилось меню Section, в котором даже во время редактирования документа содержались команды, относящиеся к Binder. С первого взгляда — вроде бы ничего особенного. Теперь давайте расставим все по местам. Представьте себе, что возможностями Binder наделены другие приложения — скажем, оболочки операционных систем. Также представьте, что такая оболочка усовершенствована и позволяет просматривать содержимое Web с такой же легкостью, с какой вы просматриваете каталоги на своем жестком диске. Итак, вы обращаетесь к Web-странице, которая содержит гиперссылку на документ Word. Допустим, на вашем компьютере Word установлен. В прежние времена вам пришлось бы загрузить документ, отдельно запустить Word и открыть в нем полученный документ, просмотреть и отредактировать его. А теперь представьте, что щелчок на гиперссылке также приводит к приему документа, однако на этот раз Word запускается как сервер Документов ActiveX прямо внутри броузера/оболочки — все готово к работе. А может, у вас нет Word? Как ни странно, Microsoft не позволит вам бесплатно получить эту программу (к тому же она великовата для пересылки по модему!), но вы можете взять программу просмотра, которая умеет точно отображать содержимое документов Word. После того как эта программа окажется на вашем компьютере, вам уже не придется принимать ее заново. Идем дальше — гиперссылки могут присутствовать в любых документах, а не только в тех, что хранятся в Web. Все гиперссылки обладают одинаковыми функциями. Например, в документ Word можно включить гиперссылку на лист Excel — если щелкнуть на ней, то на месте документа Word появляется лист Excel. При помощи кнопок Next и Previous наподобие тех, что имеются в броузерах, а также списка посещенных мест можно легко переключаться между гиперссылками на другие локальные документы, на Web-страницы и даже (поскольку это также относится к функциям оболочки) на представления локальных и сетевых устройств в Windows Explorer. Одно из следствий заключается в том, что вам не придется переводить все документы в HTMLформат, чтобы перенести их в Web — необходимо лишь, чтобы у ваших клиентов был установлен броузер с поддержкой Документов ActiveX. В определенном смысле такая оболочка становится общей платформой для работы всех приложений. А теперь давайте уточним значения некоторых терминов. Термин
Значение
Контейнер (container)
Приложение, в которое можно внедрять объекты (например, Microsoft Word).
Контейнер Приложение, обладающее функциями обычного контейнера визуального Документов ActiveX редактирования, но с дополнительными возможностями по типу Binder. Приложение объекта(object application)
Приложение, которое создает объект, внедряемый в контейнер; иногда называется «сервером объекта».
www.books-shop.com
Документ ActiveX
Документ, созданный приложением, которое поддерживает интерфейсы контейнеров Документов ActiveX, выходящие за рамки визуального редактирования.
Внедрение (embedding)
Объект «внедряется» в контейнер, когда в контейнере выделяется место для хранения всего объекта (и, следовательно, его данных); при этом не существует отдельного файла, принадлежащего приложению объекта. В моем первом примере диаграмма Excel внедряется в документ Word, если она не существует нигде за пределами этого документа.
Связывание (linking)
Объект «связывается» с контейнером, когда в контейнер включается только ссылка на него; чаще всего такой ссылкой является имя файла, в котором хранится объект. Связывание позволяет нескольким контейнерам работать с информацией от одного объекта, а изменения в объекте отражаются сразу во всех контейнерах.
Составной документ (compound document)
Документ, содержащий связанные и/или внедренные объекты из различных источников вне того приложения, в котором он был создан. Термин «документ» в данном случае следует понимать условно; сказанное справедливо и по отношению к электронной таблице, базе данных и т. д.
2.22 Составные документы Давайте вернемся к нашему примеру. Предположим, пользователь, внедривший диаграмму Excel в документ Word, сохраняет свою работу и уходит домой. Что же, собственно, он сохраняет? Как вы, наверное, догадались, для сохранения своих документов в виде составных файлов Word пользуется хранилищами и потоками. Одно из таких хранилищ при сохранении файла Word предоставляет Excel. Excel создает внутри него свой собственный набор хранилищ и потоков и сохраняет свои данные. И все же помимо этого должно сохраняться еще что-то. Когда наш пользователь приходит на следующий день и снова загружает документ в Word, объект Excel присутствует на экране, хотя Excel при этом не запускался. Конечно, при открытии составного документа можно загружать все приложения объектов, связанных с документом или внедренных в него, однако это приводит к значительным расходам времени и памяти. Приложения, способные работать с Документами ActiveX, обычно поступают иначе. Вместо этого они сохраняют графическое изображение объекта, которое называется «данными представления» (presentation data). Благодаря ему объект всегда можно воспроизвести на экране. Чаще всего такое изображение представляет собой метафайл Windows (хотя могут быть доступны и другие форматы), который может правильно отображаться при разных разрешениях экрана и при выводе на печать. Фрагмент кода, рисующий объект в контейнере при отсутствии активного сервера объекта, называется «обработчиком объекта» (object handler). Это DLL-библиотека, которая принадлежит тому же процессу, что и приложение-контейнер, и в некоторых случаях может выполнять функции сервера объекта.
Подробнее об обработчиках объектов OLE содержит стандартный обработчик, по умолчанию используемый большинством серверов объектов. Тем не менее в некоторых случаях сервер объекта должен работать с нестандартным обработчиком. Сервер объекта уведомляет об этом OLE и контейнер, включая в реестр ключ InprocHandler и/или InprocHandler32 в соответствующем подразделе HKEY_ CLASSES_ROOT\CLSID\{...}. Обработчики объектов существуют исключительно для целей оптимизации, поскольку они позволяют контейнерам обращаться к функциям внутри того же процесса. Надобность в них отпадает, если сервер объекта сам реализован в виде DLL-библиотеки (внутрипроцессный сервер), поскольку такой сервер и сам обладает преимуществами внутрипроцессного вызова. С другой стороны, использование обработчика, принятого по умолчанию, во внутрипроцессных серверах (а также в нестандартных обработчиках) иногда упрощает их реализацию.
Итак, документ открыт, и пользователь видит диаграмму Excel. Говорят, что диаграмма находится в «загруженном» состоянии, и при помощи обработчика объекта контейнер может получить указатели на различные интерфейсы, применяемые для рисования и других операций с диаграммой. Пользователь может заняться редактированием диаграммы, сделав на ней двойной щелчок. Word, пользуясь средствами OLE, попытается запустить Excel. В случае неудачи
www.books-shop.com
пользователь получает сообщение об ошибке и Word возвращается к прежнему состоянию. Если попытка окажется успешной, Excel запускается и узнает о том, что он редактирует внедренный объект. Для этого контейнер включает в командную строку Excel флаг /Embedding (точнее, включаются любые флаги, которые Excel занесет в реестр для выполнения подобных вызовов). Затем Excel получает от контейнера данные внедренного объекта и выводит их на экран. Вопрос о том, где именно будут отображаться данные объекта, отнюдь не прост. В зависимости от типов контейнера и сервера объекта, данные могут выводиться во внутренней области окна документа, принадлежащего контейнеру, или в окне, созданном контейнером для объекта, или же в окне, предоставленном самим объектом. В последнем случае это окно создается Excel. Затем Excel договаривается с Word о том, где и какие панели инструментов и другие графические объекты он должен разместить, а также какие меню и команды должны появиться в Word. На этой стадии работают и Word и Excel — говорят, что объект находится в «рабочем» состоянии. Если пользователь щелкнет в окне документа Word за пределами внедренного объекта, то Excel передает управление Word, а меню и панели инструментов возвращаются к нормальному состоянию. В противном случае все нажатия клавиш и действия с мышью передаются контейнером в Excel, если сам контейнер не хочет их обрабатывать. Говорят, что Excel находится в «UI-активном» состоянии (сокращение UI означает «пользовательский интерфейс»). UIактивность отличается от простой активности, при которой сервер работает, но не обладает собственными панелями инструментов и меню. Как мы убедимся в следующей главе, отличия между этими двумя состояниями важны для некоторых видов элементов ActiveX. Мы убедились, что просматривать внедренные объекты можно независимо от того, работает ли соответствующий сервер. Тем не менее редактирование становится возможным только при работающем сервере объекта (и это вполне логично). Чтобы перейти от просмотра к редактированию, пользователь должен сделать двойной щелчок на объекте или же выделить его и выполнить команду (обычно из меню Edit), которая приводит к UI-активизации объекта. Это означает, что сервер объекта загружается только по требованию пользователя — когда пользователь захочет изменить содержимое объекта. В терминах OLE это называется «внешней активизацией». На сегодняшний день большинство объектов активизируется извне. Причина состоит в том, что серверы объектов загружаются относительно медленно, и загрузка всех серверов при открытии документа требовала бы слишком много времени. Разработчики OLE знали, что со временем появятся и другие объекты, не такие тяжеловесные, как объекты Word или Excel. Кроме того, они предвидели, что при некоторых обстоятельствах объекты должны «оживать» сразу же после завершения их воспроизведения на экране. Для таких случаев в OLE и ActiveX поддерживается «внутренняя активизация». Некоторые виды элементов ActiveX основаны именно на внутренней активизации. Такие элементы активизируются сразу же после их отображения, поскольку они должны немедленно реагировать на действия пользователя. Кроме того, может возникнуть необходимость в асинхронной посылке «событий» контейнеру независимо от действий пользователя. Более того, элемент должен реагировать на нажатия клавиш и щелчки мышью немедленно, без предварительного двойного щелчка. Только представьте себе кнопку, на которой перед нажатием нужно сделать двойной щелчок!
ДЛЯ ВАШЕГО СВЕДЕНИЯ Многие элементы ActiveX, написанные для этой книги, представляют собой внутрипроцессные серверы (то есть DLLбиблиотеки) с внутренней активизацией. Хотя логика такого объекта может требовать внутренней активизации, не все контейнеры поддерживают ее. Следовательно, элемент ActiveX можно внедрить в старый контейнер OLE, но толку от этого будет немного. Контейнеры, написанные до выхода спецификации элементов ActiveX, также не поддерживают других возможностей этих элементов (например, событий) и не смогут пользоваться ими.
2.23 Связанные объекты Почти все сказанное о работе объектов и контейнеров с внедренными объектами относится и к связанным объектам. С точки зрения пользователя связанные объекты почти не отличаются от внедренных. Однако если при внедрении контейнер содержит всю информацию объекта, то при связывании в нем хранятся только данные представления и ссылка на источник данных. Если возникает необходимость в запуске сервера, OLE на основании сведений об источнике данных выбирает нужное приложение и находит файл, который в него следует загрузить.
www.books-shop.com
Интересное отличие между внедрением и связыванием заключается в том, что внедренные данные могут изменяться только пользователем документа, в котором они хранятся, а связанный объект может изменяться всеми, кто имеет доступ к связанному файлу. Это означает, что при активации ссылки данные представления могут оказаться устаревшими.
2.24 Документы ActiveX Третья разновидность документов — Документы ActiveX. Во многих отношениях они напоминают связанные документы, за исключением того, что просмотр или печать объекта невозможны без участия приложения-сервера. Как говорилось выше, для этого не обязательна полная версия приложения — в роли сервера может выступать любая программа, которая сообщит системе,что она умеет просматривать и/или редактировать документы соответствующего класса. Следовательно, если документ создан в Word и при этом у вас нет полной версии этого приложения, а есть лишь программа для просмотра документов Word, то она cможет правильно отобразить документ. Разумеется, если у вас нет даже такой программы, ее можно найти в Web (или в другом месте).
2.25 Drag-and-drop Одна из возможностей Документов ActiveX заключается в «перетаскивании» объектов с одного места на другое — скажем, из диаграммы Excel в документ Word. Слышу ваш недоуменный вопрос: «А причем здесь OLE»? Конечно, drag-and-drop можно реализовать и без помощи OLE, как это было сделано в File Manager из Microsoft Windows 3.1. Пользователь мог перетащить файл из окна File Manager и бросить его в любом окне, которое зарегистрировалось в качестве приемника (drop target). Затем соответствующее окно получало сообщение WM_DROPFILES. Здесь и скрыта разгадка — такая реализация drag-and-drop ориентирована на работу с файлами, ее трудно приспособить для более общих целей. Кроме того, вы можете без особых проблем запрограммировать drag-and-drop самостоятельно, но тогда каждое приложение, которое становится источником или приемником, должно поддерживать drag-and-drop в соответствии с вашим определением. Соответственно, возникает потребность в реализации drag-and-drop на системном уровне. Поскольку OLE обладает рядом других полезных возможностей, Microsoft решила построить drag-and-drop на основе механизма OLE. Drag-and-drop на основе OLE поддерживает перетаскивание в окне приложения, между окнами приложения и между приложениями. В сущности, результат почти не отличается от вырезания и вставки (cut-and-paste), однако процесс выглядит более естественно, чем работа с командами меню и сочетаниями клавиш. Поскольку Clipboard может содержать данные различных форматов, в том числе и определяемых пользователем, средства drag-and-drop на основе OLE оказываются значительно полезнее тех, которые предоставлялись File Manager. Drag-and-drop в первую очередь предназначается для работы с документами и потому считается составной частью группы Документов ActiveX. Обеспечить поддержку drag-and-drop на основе OLE достаточно просто. Для источника, с которого пользователи могут перетаскивать информацию в другое место, необходимо реализовать интерфейс IDropSource и поддерживать интерфейс IDataObject (о котором мы поговорим чуть позже). Чтобы принимать перетаскиваемые данные, необходимо реализовать IDropTarget и уметь работать с указателем IDataObject. Интерфейс IDropSource, в дополнение к методам IUnknown, содержит еще два метода: QueryContinueDrag и GiveFeedback. QueryContinueDrag вызывается, когда пользователь бросает данные или предпринимает какие-то действия, способные отменить перетаскивание. GiveFeedback вызывается для того, чтобы приложение-источник могло определить вид курсора во время перетаскивания. Но если на этом функции IDropSource ограничиваются, как же приложение-получатель должно понять, что именно на него перетащили? Когда источник начинает операцию перетаскивания, он вызывает функцию API с именем DoDragDrop. Этой функции передаются указатели на интерфейсы IDataObject и IDropSource источника, а также набор флагов, которые определяют виды разрешенных действий. Приложение, которое собирается получать перетаскиваемые объекты, должно вызвать функцию RegisterDragDrop для каждого окна-приемника. Ее параметрами является логический номер окна и указатель на интерфейс IDrop Target. Методы IDropTarget вызываются во время перетаскивания. Некоторые из них — DragEnter, DragOver и DragLeave — вызываются при перемещении курсора над окном-приемником. Если отпустить перетаскиваемый объект в этом Ⱦɚɧɧɚɹɜɟɪɫɢɹɤɧɢɝɢɜɵɩɭɳɟɧɚɷɥɟɤɬɪɨɧɧɵɦɢɡɞɚɬɟɥɶɫɬɜɨɦ%RRNVVKRS ɊɚɫɩɪɨɫɬɪɚɧɟɧɢɟɩɪɨɞɚɠɚɩɟɪɟɡɚɩɢɫɶɞɚɧɧɨɣɤɧɢɝɢɢɥɢɟɟɱɚɫɬɟɣɁȺɉɊȿɓȿɇɕ Ɉɜɫɟɯɧɚɪɭɲɟɧɢɹɯɩɪɨɫɶɛɚɫɨɨɛɳɚɬɶɩɨɚɞɪɟɫɭ[email protected]
окне, будет вызван метод IDropTarget::Drop для соответствующего окна. Вызванному методу передается указатель на реализацию IDataObject перетаскиваемого объекта.
2.26 Интерфейсы Документов OLE и ActiveX Имена почти всех интерфейсов, входящих в семейства Документов OLE и Документов ActiveX, начинаются с IOle. В семейство Документов OLE входят интерфейсы IOleAdviseHolder, IOleCache, IOleCache2, IOleCacheControl, IOleClientSite, IOleContainer, IOleInPlaceActiveObject, IOleInPlaceFrame, IOleInPlace Object, IOleInPlaceSite, IOleInPlaceUIWindow, IOleItemContainer, IOleLink, IOleObject и IOleWindow! В семействе Документов ActiveX к ним добавляются IOleCommand Target, IOleDocumentSite, IOleDocument, IOleDocumentView, IPrint и IContinueCallback. Кроме этих интерфейсов, заслуживает упоминания (в который раз!) интерфейс IDataObject, а вместе с ним и множество других. Я не собираюсь подробно рассматривать все эти интерфейсы. Тем не менее при изучении Документов OLE и ActiveX необходимо учитывать наличие двух сторон: внедряемых и/или связываемых объектов и контейнеров, в которых они хранятся. Чтобы визуальное редактирование нормально работало, обе стороны должны проделать огромную подготовку и много общаться друг с другом. Давайте сначала рассмотрим ситуацию с позиции контейнера. Итак, пользователь хочет вставить объект в документ-контей нер (следует учитывать, что термин «документ» используется условно — для управляющих элементов более правильным был бы термин «экранная форма»). Для этого существует много способов: перетаскивание из другого окна (и, возможно, из другого приложения), вставка из Clipboard, чтение из файла на диске и выбор объекта в окне диалога. В конечном счете все они приводят к одному и тому же процессу, поэтому я остановлюсь на последнем варианте — окне диалога, в котором пользователь выбирает вставляемый объект. Чаще всего для этой цели используется стандартное окно диалога Insert Object, которое можно встретить во многих приложениях-контейнерах (например, во всех продуктах Microsoft Office — Word, Excel, Access и т. д.). Каким образом заполняется это окно? Вероятно, вы уже догадались, что OLE для этого пользуется основным источником всех сведений об объектах — реестром. Тем не менее далеко не каждый зарегистрированный класс должен присутствовать в этом окне диалога. Как же OLE отбирает нужные объекты? Конечно, было бы предельно глупо создавать экземпляры объектов всех классов только для того, чтобы спросить у них, поддерживают ли они интерфейсы Документов OLE. Вместо этого OLE ищет в разделе реестра HKEY_CLASSES_ROOT\CLSID\{...} для каждого сервера ключ с именем Insertable. Если такой ключ присутствует, значит, сервер поддерживает создание своих объектов через интерфейсы Документов OLE — и соответствующий класс может быть с полным правом включен в окно диалога Insert Object.
ЗАМЕЧАНИЕ По крайней мере в наши дни большинство контейнеров распознает вставляемые объекты именно таким способом. Тем не менее после выхода в 1996 году спецификации компонентных категорий контейнеры и объекты постепенно переходят от использования ключа Insertable к соответствующим компонентным категориям. Более того, для упрощения перехода менеджер компонентных категорий устанавливает двустороннее соответствие между ключом и категорией. Это означает, что приложения, которые ищут в реестре CLSID с ключом Insertable, как по волшебству найдут и те, для которых установлен признак соответствующей категории. И наоборот — новые приложения, которые ищут объекты по категории, найдут и те, для которых используется ключ Insertable.
После того как пользователь выберет из списка требуемый объект, контейнер может попытаться создать экземпляр обычным способом, через вызов CoCreateInstance. Однако в OLE имеется функция API с именем OleCreate, которая делает то же самое и многое другое. Обычно контейнеры предпочитают пользоваться OleCreate. Некоторые из получаемых ею параметров аналогичны параметрам CoCreateInstance — например, CLSID нужного класса (полученный из окна диалога Insert Object) и REFIID нужного интерфейса (обычно контейнеры запрашивают интерфейс IOleObject). Однако в данном случае не передается указатель на управляющий IUnknown, поскольку контейнеры не объединяются с вставляемыми в них объектами. Дополнительные параметры сообщают OLE и объекту, каким образом контейнер хочет воспроизводить объект на экране, а указатель на интерфейс IStorage определяет хранилище,
www.books-shop.com
используемое для объекта. Наконец, OleCreate получает указатель на интерфейс клиентского узла IOleClientSite, который используется в вызове IOleObject::SetClientSite для создаваемого объекта. Кстати, что такое «клиентский узел»? Вообще говоря, клиентский узел предоставляет средства для взаимодействия внедренного или связанного объекта с контейнером. Он поставляет объекту сведения о физическом расположении объекта в документе-контейнере и другую необходимую информацию, относящуюся к контейнеру. Контейнер должен создать клиентский узел для каждого связанного или внедренного объекта. С клиентскими узлами связана одна тонкость: я могу вполне естественно сказать что-нибудь вроде «указатель на интерфейс IOleObject нашего объекта хранится в контейнере», но на самом деле это неверно. Указатель хранится в клиентском узле. Почему это так важно? Потому что каждый клиентский узел отличается от других и отвечает только за свой объект. Контейнер, в свою очередь, отвечает за клиентские узлы. Вы можете определить базовый контейнер, поддерживающий внедрение объектов — такой контейнер понимает значение клиентских узлов в своем контексте и реализует интерфейс IOleClient. Кроме того, контейнер должен в обязательном порядке реализовать интерфейс IAdviseSink, через который объекты сообщают ему об изменениях в данных. Аналогично можно определить и простые объекты, предназначенные для внедрения, — они должны реализовывать или предоставлять интерфейсы IOleObject, IDataObject, IOleCache, IPersistStorage и IViewObject. Почему я выделил слова «или предоставлять»? Потому что объект может делегировать некоторые из этих интерфейсов компонентам OLE. Например, интерфейс IViewObject часто (даже почти всегда) реализуется стандартным обработчиком объекта, принадлежащем OLE и расположенном в OLE32.DLL. Интерфейсы объектов имеют следующее назначение: Интерфейс IOleObject
Назначение «Душа» объекта. Интерфейс образует глубокую и содержательную связь с клиентским узлом.
IDataObject
Способ получить данные от объекта в нужном формате (который должен поддерживаться объектом).
IViewObject
Отчасти похож на IDataObject, однако используется для получения изображения объекта, а не его данных. Некоторые методы IViewObject получают HDC (логический номер контекста устройства), который не может передаваться между процессами. Следовательно, IViewObject необычен в том отношении, что для него не выполняется маршалинг и потому он может выполняться только в DLL — еще одна веская причина, по которой большинство серверов поручает этот интерфейс стандартному обработчику OLE!
Поддержка структурированного хранения со стороны объекта. Контейнер IPersistStorage обращается к интерфейсу IPersistStorage объекта для того, чтобы тот выполнил сериализацию для передаваемого IStorage. IOleCache
Интерфейс реализуется OLE. Объекты пользуются им для того, чтобы управлять кэшированием (промежуточным хранением) данных во внедренном объекте и выбором данных, доступных контейнеру при отсутствии работающего сервера.
Контейнеры и объекты Документов OLE могут пользоваться и другими интерфейсами, обеспечивающими дополнительные возможности. Например, ни один из интерфейсов в приведенной выше таблице не обеспечивает возможности визуального редактирования и даже активизации объектов: для этого контейнеры и серверы объектов должны реализовать интерфейсы IOleInPlacexxx. Один интересный метод, IOleInPlaceActiveObject::TranslateAccelerator, вызывается для каждого активного объекта после каждого нажатия клавиш, чтобы внедренные объекты смогли отреагировать на свои клавиши-акселераторы. Первый же активный объект, который обработает нажатые клавиши, прекращает поиск. Я особо выделил этот метод, поскольку для элементов ActiveX, в которых используется внутренняя активизация, реализацию IOleInPlaceActiveObject необходимо усовершенствовать. Интерфейсы ActiveX для контейнеров и документов Естественно, превращение обычного составного документа или контейнера в документ или контейнер ActiveX должен сопровождаться реализацией и использованием ряда новых
www.books-shop.com
интерфейсов. Например, объекты Документов ActiveX поддерживают один или несколько видов своих данных — это как раз то, что вы видите внутри контейнера. В качестве примера можно упомянуть режимы просмотра в Word — обычный, структурный, разметки и т. д. В следующей таблице перечисляются дополнительные интерфейсы, используемые контейнерами. Интерфейс
Назначение
Через этот необязательный интерфейс контейнер может получать команды от объекта. Обычно такие команды обусловлены действиями пользователя (работой с меню или панелями инструментов), поэтому интерфейс также используется для получения информации о состоянии флажков (то есть IOleCommandTarget включаемых команд) в меню, чтобы при их установке объект мог отобразить соответствующую информацию. Не стоит рассматривать этот интерфейс как замену Automation. Это всего лишь простое средство для передачи команд меню от одного объекта к другому.
IOleDocumentSite
Этот интерфейс на редкость прост. Он содержит всего один метод, ActivateMe, при помощи которого вид обращается к соответствующему узлу документа с требованием активировать объект. Он заменяет все переговоры и тяжелую подготовительную работу, которой обычно сопровождается активизация объектов. Если учесть, что объект Документов ActiveX активируется именно как документ, а не как объект внутри другого документа, использование интерфейса явно упрощает процесс.
IContinueCallback
Этот необязательный интерфейс позволяет объекту спросить у контейнера, следует ли ему продолжать некоторую, обычно длительную операцию. Интерфейс применяется в первую очередь для печати, однако он достаточно универсален, чтобы его можно было использовать практически для любой аналогичной цели.
В следующей таблице рассматриваются интерфейсы ActiveX, относящиеся к документам. Интерфейс
Назначение
IOleDocument
Интерфейс отделяет объекты ActiveX от стандартных объектов с активизацией на месте. В нем имеется метод для создания вида и другой метод, который перечисляет различные виды, предоставляемые объектом в настоящий момент. Он также содержит метод для получения различных флагов из реестра, которые сообщают контейнеру сведения об объекте — например, может ли объект поддерживать несколько видов или «сложных прямоугольников», что фактически означает способность принять массив прямоугольников с размерами и координатами для самого вида, его полос прокрутки и кнопки масштабирования.
IOleDocumentView
Каждый вид документа представляет собой вспомогательный объект, реализующий данный интерфейс вместе со всеми интерфейсами, необходимыми для активизации на месте. Интерфейс позволяет контейнеру получить и задать объект-узел вида, получить и задать его прямоугольник и выполнить базовые действия по активизации — отображение, UI-активизацию и закрытие в обход обычного механизма активизации на месте.
IPrint
Через этот (необязательный) интерфейс контейнер может потребовать у объекта напечатать его документ из программы вместо печати через команды пользовательского интерфейса. В число параметров метода Print данного интерфейса входит указатель на интерфейс IcontinueCallback объекта, по которому объект может определить, следует ли ему продолжать печать.
IOleCommandTarget
Через этот необязательный интерфейс объект может получать команды от контейнера. Обычно такие команды обусловлены действиями пользователя (работой с меню или панелями инструментов), поэтому интерфейс также используется для получения информации о состоянии флажков (то есть включаемых команд) в меню, чтобы при их установке контейнер мог отобразить соответствующую информацию. Не стоит рассматривать этот интерфейс как замену Automation. Это всего лишь
www.books-shop.com
простое средство для передачи команд меню от одного объекта к другому. IEnumOleDocumentViews
Стандартный интерфейс OLE, который перебирает виды из набора, поддерживаемого данным сервером Документов ActiveX.
2.27 Другие интерфейсы ActiveX Чтобы привести эту главу к логическому завершению, мы рассмотрим несколько оставшихся интерфейсов ActiveX и OLE, которые также заслуживают нашего внимания. Я не стану вдаваться в подробности, поскольку эта информация скорее интересна, нежели жизненно необходима. Некоторые из этих интерфейсов используются или реализуются элементами ActiveX, но по своей значимости они уступают, например, Automation. Самым важным из них является интерфейс IDataObject; я неоднократно упоминал о нем раньше, однако опишу только сейчас.
2.28 IDataObject На интерфейсе IDataObject построено такое средство OLE, как «единый механизм передачи данных», или UDT. Общая идея заключается в том, что механизм обмена данными должен оставаться постоянным независимо от конкретного способа. Неважно, перетаскиваете ли вы данные мышью, копируете через Clipboard, пользуетесь программными или любыми другими средствами — получателю данных передается указатель на реализацию IDataObjectобъекта, предоставляющего данные. Затем получатель извлекает данные при помощи методов этого интерфейса. Интерфейс IDataObject обладает большими возможностями, чем Clipboard, поскольку он позволяет передавать данные в более широком диапазоне типов и пользоваться более разнообразными «промежуточными носителями». Гибкость IDataObject обусловлена применением двух структур: FORMATETC и STGMEDIUM. Первая, FORMATETC (произносится «формат эт сетера»), определяет формат, в котором данные будут извлекаться из IDataObject. Подобно тому, как приложение может заносить данные в Clipboard сразу в нескольких форматах, так и IDataObject может работать с несколькими FORMATETC. Однако в отличие от форматов данных Clipboard, FORMATETC позволяет задавать дополнительную информацию — наибольший интерес представляет устройство-приемник (если оно имеется) и «аспект» данных. Пока определены такие аспекты, как содержимое (то есть собственно данные), миниатюра (уменьшенное представление), пиктограмма и печатный документ. Также можно задать промежуточный носитель, посредством которого должна осуществляться передача — совместная область памяти (через глобальный логический номер), файл, IStorage, IStream, растр или метафайл. Вторая структура данных, STGMEDIUM, используется для передачи информации о промежуточном носителе. В эту структуру входит флаг, определяющий сущность носителя (как в упомянутом выше поле структуры FORMATETC); объединение для хранения ссылки (глобального логического номера области памяти, логического номера растра или указателя на IStream); и указатель на IUnknown. Если последний не равен NULL, то он должен использоваться для освобождения промежуточного носителя (посредством вызова IUnknown::Release). В следующей таблице перечислены методы IDataObject, представляющие наибольший интерес. Метод GetData GetDataHere
Описание Ключевой метод интерфейса. Предназначен для занесения данных объекта в заданном формате на заданный промежуточный носитель. Аналогичен GetData, за исключением того, что данный метод извлекает данные и заносит их на промежуточный носитель, указанный при вызове.
QueryGetData Проверяет, успешно ли закончится вызов GetData при заданном FORMATETC. Возвращает итератор для перебора форматов, в которых заданный IDataObject EnumFormatEtc готов послать свои данные. Итератор представляет собой указатель на интерфейс IEnumFormatEtc. DAdvise
Создает информационную связь между объектом данных и кем-то, желающим знать об изменении данных. Заинтересованная сторона передает этому методу указатель на свой интерфейс IAdvise Sink. Объекты данных могут принимать
www.books-shop.com
сразу несколько запросов на уведомление, поэтому часто подобные извещения обрабатываются совместно, через интерфейс IDataAdviseHolder. IMoniker Тони Уильямс, главный архитектор OLE, — англичанин (на мой взгляд, крайне полезное качество для программиста!). Именно поэтому на конференции разработчиков OLE в Сиэттле в мае 1993 года Microsoft пришлось объяснять, что такое «моникер» (почти все присутствующие были американцами). Я тоже англичанин, поэтому для меня слово «моникер» обозначает то же, что и для Тони — имя или прозвище для чего-нибудь. Если хотите — своего рода логический номер. Термин «моникер» в ActiveX относится к объекту, который реализует интерфейс IMoniker и ссылается на другой, связанный с ним объект. Моникеры можно использовать не только для ссылок — например, они встречались нам при обсуждении RegisterActiveObject в разделе этой главы, посвященном Automation. Впрочем, мы все равно имеем дело со ссылкой, потому что в данном случае моникер предоставляет механизм, посредством которого контроллер Automation может получить доступ к внешнему рабочему объекту. Вместо того чтобы включать в контейнер имя связанного файла, ActiveX заносит в него один или несколько моникеров. Набор моникеров однозначно определяет местонахождение файла. Ссылки часто сохраняются в виде относительного или абсолютного пути, поэтому в большинстве случаев связанный файл можно найти даже после его перемещения. Моникеры бывают разными. В OLE 2.0 поддерживались следующие типы:
«Составным моникером» называется упорядоченная последовательность других моникеров. «Файловые моникеры» представляют файловые пути; они всегда стоят в левой части составных моникеров. «Позиционные моникеры» определяют конкретную позицию в объекте — например, диапазон ячеек в электронной таблице. «Антимоникеры» нейтрализуют действие моникеров, расположенных непосредственно слева от них. «Моникеры указателей» представляют собой оболочки для обычных указателей.
Самая обычная операция с моникером, осуществляемая через интерфейс IMoniker, называется «связыванием» (binding). Это означает получение объекта, на который ссылается моникер. Например, метод IMoniker::BindToObject получает моникер, запускает объект, на который он ссылается, и возвращает указатель на требуемый интерфейс. Метод IMoniker::BindToStorage делает почти то же самое, но возвращает хранилище объекта. Дальнейший вызов метода Load позволяет извлечь объект из хранилища и запустить его. В первом издании книги я написал: «Для нас, как для разработчиков элементов OLE, моникеры не имеют особого значения. Впрочем, они составляют довольно интересный аспект работы OLE». Признаю свою ошибку! С того времени моникеры стали чрезвычайно важны и для разработчиков элементов. Получилось так, что в дополнение к стандартным именам файлов, существующая архитектура моникеров прекрасно подошла и для работы с URL. Кроме того, поскольку URL обычно ссылаются на удаленные системы, операция связывания далеко не всегда является атомарной — другими словами, возникает необходимость в асинхронном выполнении связывания по отношению к другим операциям с объектом. Соответственно, Microsoft ввела новый тип моникеров — асинхронные. Для нас, разработчиков элементов, особое значение имеет один частный случай моникеров этого типа — URL-моникеры. Отличие асинхронного моникера от обычного, синхронного, заключается в том, что при выполнении запроса на связывание передаваемая ему информация включает указатель на новый интерфейс, IBindStatusCallback. Этот интерфейс реализуется программой, выдающей запрос на связывание (если хотите — клиентом моникера), а его методы вызываются в соответствующие моменты связывания. Например, метод OnStartBinding вызывается в начале операции связывания, OnObjectAvailable — в момент, когда становится доступным весь связанный объект, а OnStopBinding — при завершении связывания. Также заслуживает внимания метод GetPriority этого интерфейса. Пользуясь им, клиент может задать для моникера относительную важность конкретной операции связывания по сравнению с остальными, которые могут выполняться в то же время для того же клиента. Почему он заслуживает внимания? Рассмотрим обычную Web-
www.books-shop.com
страницу. Вполне возможно, что сразу несколько расположенных на ней объектов (чаще всего элементов, хотя возможны и другие варианты) будут обладать частями, связывание которых осуществляется асинхронно (например, элементы с одним или несколькими BLOB-свойствами. Используя механизм приоритетов (а также многопоточные возможности операционной системы), контейнер может указать, какие связывания на странице важнее других и должны осуществляться в первую очередь. Наконец, приходится учитывать, что элемент может иметь асинхронные («путевые») свойства; если их будет несколько, то в будущем может появиться механизм, который позволял бы задать порядок получения этих свойств. Кроме того, в некоторый момент времени у связывания может существовать сразу несколько клиентов (например, если элемент используется в нескольких местах Web-страницы или если две загружаемые страницы пользуются одним и тем же элементом). Те же самые асинхронные моникеры позволяют одновременно работать с несколькими клиентами связывания. Методу OnStartBinding, вызываемому в начале связывания, также передается новый интерфейс IBinding. Методы этого интерфейса могут применяться клиентом для приостановки или отмены операции связывания, а также для чтения или установки ее приоритета. Наконец, асинхронные моникеры могут пользоваться другой возможностью ActiveX — асинхронным хранением. Для этого предназначен новый интерфейс, IPersistMoniker. Это самый гибкий из имеющихся механизмов обеспечения устойчивости (интерфейсов IPersistxxx), потому что даже само понятие устойчивости в нем может быть асинхронным. Кстати, асинхронные моникеры поддерживают еще один новый интерфейс — IAsyncMoniker. Реально этот интерфейс не существует (это всего лишь IUnknown), но по нему можно определить, является ли данный моникер асинхронным. Достаточно вызвать для него QueryInterface и запросить указатель на IAsyncMoniker. Если в результате будет получен нормальный указатель, то моникер асинхронный, в противном случае — нет. Почему я сказал, что URL-моникеры представляют собой частный случай асинхронных моникеров? Можно провести параллель с файловыми моникерами, которые «понимают» файловые имена и соответствуют им. Например, URL-моникер может представлять полный или относительный URL (по аналогии с относительными файловыми путями). При необходимости построения полного URL моникер может обратиться к контексту связывания. Метод CreateURLMoniker применяется для создания как полных, так и относительных URL-моникеров. Используемая моникерами структура FORMATETC содержит новые типы, относящиеся к стандарту MIME — например, CF_MIME_POSTSCRIPT, а метод RegisterMediaTypes позволяет задать новые типы. Наконец, URL-моникер может потребовать у клиента выполнения некоторых действий во время связывания — например, проверки и изменения HTTP-заголовков. Для этого она вызывает моникер QueryInterface интерфейса IBindStatusCallback, раскрываемого клиентом. В настоящее время определены два интерфейса, которые можно запрашивать подобным образом: IAuthentificate и IHttpNegotiate.
2.29 IRunningObjectTable «Таблица рабочих объектов», или ROT, создана в первую очередь для хранения ссылок на выполняемые объекты, благодаря которым ускоряется связывание моникеров с существующими объектами. Ее также можно рассматривать как общий список выполняемых объектов OLE, однако следует помнить, что в список включаются лишь те объекты, которые сами того пожелают. Функция API GetRunningObjectTable возвращает указатель на интерфейс IRunningObjectTable, при помощи которого можно добавлять и удалять элементы таблицы, а также проверить, выполняется ли объект с заданным моникером, и получить указатель на интерфейс IEnumMoniker. Этот интерфейс предназначен для перебора элементов ROT. Конечно, в нем имеется метод для получения указателя IUnknown по элементу таблицы. В Microsoft Visual C++ входит утилита IRotView, которая динамически отображает содержимое ROT.
2.30 Как больше узнать об ActiveX
www.books-shop.com
В этой главе мы быстро промчались по стране ActiveX и OLE, остановились во многих местах — но лишь на считанные минуты. В принципе этих знаний об ActiveX вполне достаточно, — если хотите, можно немедленно приступать к созданию элементов — в таком случае пропустите главу 3 и переходите прямо к главе 4. С другой стороны, если вас все еще мучает вопрос «а что там внутри?», читайте главу 3.
www.books-shop.com
Глава
3
COM-расширения для элементов Эта глава продолжает тему предыдущей и еще на один шаг продвигается в исследовании тех аспектов ActiveX и COM, которые относятся непосредственно к разработке элементов. Для начала мы рассмотрим небольшую программу, использующую Automation (даже не элемент ActiveX!), а затем добавим к ней некоторые возможности, основанные на структурированном хранении. Затем мы познакомимся с архитектурой Элементов ActiveX и посмотрим, как OLE и COM были расширены для поддержки новых возможностей. Слово «расширены» вовсе не подразумевает выпуска новой версии COM и OLE или даже каких-либо изменений в стандартных DLL-библиотеках COM и OLE. Как было показано в прошлой главе, расширение OLE сводится к определению новых интерфейсов и, возможно, созданию дополнительных DLL-библиотек, которые обеспечивают их работу.
ЗАМЕЧАНИЕ Библиотека Microsoft Foundation Classes (MFC), Microsoft Visual Basic 5.0, Microsoft Visual J++ и другие средства разработчика, рассмотренные в главе 4, успешно скрывают от программистов многочисленные технические подробности, относящиеся к архитектуре Элементов ActiveX. Тем не менее для знакомства с некоторыми темами, рассмотренными в этой книге, необходимо понимание базовых принципов, на которых основана их работа. Если технические подробности вас не интересуют, переходите к главе 4. В противном случае продолжайте читать, поскольку в этой главе мы познакомимся с ними и рассмотрим важнейшие моменты.
Несколько слов о Unicode Большинство современных операционных систем для персональных компьютеров работает с 8-разрядным набором символов — обычно это символы в кодировке IBM PC или ANSI. Все прекрасно, если вы живете в англоязычной стране, и относительно неплохо, если ваш национальный алфавит совпадает с английским (возможно, с добавлением таких символов, как Я, Е или Й). Если же в вашей стране используется совершенно иной набор символов, придется подождать, пока для вас выпустят специальную версию операционной системы (примером могут послужить дальневосточные версии Microsoft Windows) или же смириться с использованием англоязычной (американской) версии. В специализированных версиях используются символы с кодировкой переменной длины из набора MCBS (многобайтовой кодировки, multi-byte character set). В течение некоторого времени консорциум Unicode пытался выработать более удачное решение для поддержки национальных алфавитов. Unicode — так называется набор символов, ставший результатом их трудов. Все символы Unicode имеют длину в 16 бит, благодаря чему они интерпретируются быстрее символов MCBS (так как символы Unicode имеют фиксированную длину), а в наборе хватает достаточно места для представления 65,536 различных символов. В консорциум входит и фирма
www.books-shop.com
Microsoft. Некоторые части набора Unicode изначально считаются зарезервированными. Например, символы с кодами от 32 до 127 совпадают со своими ASCII-эквивалентами. Другие символы используются для работы с иероглифами дальневосточных языков, кириллицей и т. д. Основная проблема заключается в следующем: если ни одна программа не понимает кодировку Unicode, то как ей пользоваться? Вся работа операционной системы Microsoft Windows NT происходит исключительно в кодировке Unicode. Это означает, что во внутреннем представлении всех символов и строк используется Unicode, а все передаваемые ASCII- и ANSI-строки перед дальнейшей обработкой преобразуются в Unicode. Все сообщения об ошибках, системные сообщения и т. д. также хранятся в кодировке Unicode. 32-разрядный протокол COM тоже основан на Unicode. Это обстоятельство выглядит особенно оригинально, если учесть, что некоторые разновидности Win32 API (например, те, которые поставляются с текущими версиями Windows 95) продолжают работать в кодировке ANSI. Тем не менее даже на этих платформах COM работает в Unicode. В листингах программы-примера, начинающихся на стр. 96, все передаваемые или полученные от ActiveX API строки преобразуются в Unicode. Функции преобразования существуют на всех платформах Win32; без них нам пришлось бы довольно туго. Если внести в программу несколько небольших изменений, чтобы кодировка Unicode использовалась повсеместно (а не только в обращениях к ActiveX API), а затем перекомпилировать ее для Unicode, то программа будет работать только в 32-разрядном режиме на Windows NT, а при попытке запустить ее в любых других условиях программа работать не будет. Возникает вопрос — почему? Дело в том, что, как я уже говорил, 32-разрядный COM работает исключительно с Unicode — он не принимает и не возвращает ASCII-строк. Различные утилиты и языки программирования скрывают это обстоятельство, осуществляя «оперативные» преобразования между ANSI и Unicode при обращениях к ActiveX. Windows 95 практически не понимает кодировки Unicode — в этой системе предусмотрены лишь отдельные функции API, способные работать с Unicode и выполняющие самые простые задачи. Разумеется, все «родные» Unicode-программы будут несколько быстрее работать под Windows NT, нежели программы «с прослойками», зато нигде больше они работать не будут. Запомните следующее правило: компилировать программу конкретно для Unicode следует лишь в том случае, если она должна работать только в Windows NT. В противном случае компилируйте для ANSI и подумайте об отдельной Unicode-компиляции для Windows NT. Построение Unicode-приложений на Microsoft Visual C++ выполняется следующим образом: 1.
2.
Проследите за тем, чтобы вместо обычных объявлений переменных вроде char или char* использовались макросы типов из WCHAR.H — такие, как TCHAR и LPTSTR (класс CString из библиотеки MFC поддерживает Unicode, начиная с версии MFC 3.0 и выше, поэтому изменять объявления переменных CString не нужно). Эти макросы обеспечивают правильный выбор типа переменной в зависимости от типа компиляции, ANSI или Unicode. Проследите за тем, чтобы все строковые константы в вашей программе находились внутри макросов, обеспечивающих их компиляцию в ANSI или Unicode. Например, строковую константу можно задать следующим образом:
_T("String") 3.
Удалите из командной строки компоновки все ключи _MBCS или _DBCS и добавьте в нее _UNICODE.
www.books-shop.com
4. 5.
Замените точку входа программы на wWinMainCRTStartup. Постройте заново весь проект.
Все макросы и типы, применяемые при программировании для Unicode, также определены и в 16-разрядном Visual C++ версий 1.51 и выше. Это означает, что использующие их программы могут быть перекомпилированы под 16-разрядную модель. 16-разрядные Windows-приложения и даже 16разрядные приложения, работающие в Windows NT, не поддерживают Unicode. Описанные выше действия используются во всех программах и примерах, встречающихся в книге, — за исключением самой первой программы для работы с Automation, которую я намеренно постарался сделать как можно проще.
3.1 Пример работы с объектом Automation Лично я разделяю мнение о том, что для более полного усвоения нового материала необходимо активное участие, а не пассивное чтение. Итак, сейчас мы займемся написанием примитивной программы на C++, которая реализует простейший объект Automation на C++. Этот объект обладает всего одним свойством, Salary, и всего одним методом Payraise. Salary — длинное целое, доступное для записи и чтения; Payraise получает длинное целое и прибавляет его к текущему значению свойства Salary. Что-нибудь проще даже придумать трудно! Работу объекта мы проверим с помощью приведенного ниже фрагмента на Visual Basic. Этот фрагмент также будет работать в любом приложении, поддерживающем Visual Basic for Applications:
Sub TestObj() Dim x As Object Set x = CreateObject("AutoProg.Cauto.2") x.Salary = 1234 MsgBox x.Salary x.Payraise 1 Msgbox x.Salary Set x = Nothing End Sub Этот фрагмент создает объект, устанавливает значение свойства Salary равным 1234, получает и выводит это значение (чтобы убедиться, что все работает нормально). Затем он вызывает метод Payraise с приращением 1, послечего снова получает и выводит это значение (для проверки обновления свойства). Наконец, строка
Set x = Nothing заставляет Visual Basic вызвать метод Release для указателя на интерфейс IDispatch объекта. Это необходимо сделать, чтобы компенсировать неявный вызов AddRef при создании объекта посредством CreateObject. Счетчик ссылок объекта падает до 0. Тем не менее наш объект также поддерживает раннее связывание (через библиотеку типов) и связывание через v-таблицу (то есть реализует двойственный интерфейс). Можно значительно повысить эффективность фрагмента на Visual Basic, если заменить его следующим:
MsgBox x.Salary x.Payraise 1 MsgBox x.Salary End Sub В данном случае строка
Dim x As New CAuto заменяет строки
Dim x As Object Set x = CreateObject("AutoProg.Cauto.2") и объявляет, что переменная x относится к конкретному типу — CAuto. Как мы вскоре увидим, определение типа CAuto находится в библиотеке типов, к которой Visual Basic обращается во время написания программы для проверки имен свойств и методов, типов и параметров. Кроме того, Visual Basic замечает, что интерфейс является двойственным, и обращается к нему через v-таблицу, в обход IDispatch. Когда вы займетесь построением данного примера, попробуйте воспользоваться первым фрагментом на Visual Basic для вызова IDispatch::Invoke и сравните два варианта вызова — двойственный интерфейс работает намного эффективнее! Наконец, наш объект получается относительно компактным. Я пользуюсь MFC для того, чтобы облегчить программирование части объекта, относящейся к Windows, я пользуюсь обработкой исключений и runtime-библиотекой C. Даже с учетом всего этого EXE-файл в окончательной (release) версии имеет размер всего в 9 Кб. Существуют специальные средства для C++ (такие, как ActiveX Template Library фирмы Microsoft), которые могут заметно сократить даже этот небольшой объем. Наконец, если вы читали первое издание этой книги, OLE Controls Inside Out, то наверняка заметили, что в новом издании эта программа заметно изменилась. Старая версия не поддерживала двойственных интерфейсов, не имела библиотеки типов и пользовалась устаревшими структурами и вспомогательными API для «имитации» библиотеки типов. В новом издании я решил в корне изменить ситуацию и сделать так, чтобы наша программа больше напоминала «настоящие» объекты Automation. Оказывается, при этом получается более понятный и к тому же более компактный код! Хотя наше приложение включает библиотеку типов, которую необходимо откомпилировать для запуска программы (на самом деле это происходит автоматически во время построения приложения), я не стану описывать ни библиотеку типов, ни ее исходный текст (ODL-файл) до тех пор, пока не завершу полное описание всей реализации приложения. Дело в том, что библиотеки типов требуют основательного разговора, а я предпочитаю выложить материал за один раз вместо того, чтобы метаться туда-сюда при описании программы. Надеюсь, такой подход упростит чтение и для вас.
3.2 Краткое знакомство с объектом Приведенный в этом разделе объект Automation написан на базе библиотеки Microsoft Foundation Classes (или сокращенно MFC), что упрощает его программирование в среде Windows, однако при этом он обходится без какой-либо существенной поддержки ActiveX, предусмотренной в MFC. Кроме того, хотя объект содержит три интерфейса, я написал код для поддержки лишь двух из них (IClassFactory и двойственный интерфейс IAuto). Как мы убедимся, в ActiveX имеются функции API, благодаря которым создание IDispatch происходит почти автоматически и заметно упрощается по сравнению с самостоятельной реализацией интерфейса.
ЗАМЕЧАНИЕ
www.books-shop.com
Подобный подход связан с некоторыми ограничениями, которые будут перечислены в конце описания программы.
Эта глава не является руководством по MFC для новичков (если вы учитесь создавать элементы ActiveX на базе MFC, обращайтесь к приложению A), так что я не стану особенно углубляться в те аспекты происходящего, которые связаны с Windows. Книги Джеффа Просиса (Jeff Prosise) «Programming Windows 95 with MFC» или Дэвида Круглински (David Kruglinski) «Inside Visual C++» гораздо лучше познакомят читателя с внутренним устройством MFC. А я лишь скажу, что CWinApp — это класс, в котором находится цикл сообщений приложения (цикл, который получает от Windows сообщения для приложения и распределяет их между окнами), а CFrameWnd — общий класс обрамленного окна. Функции InitInstance и ExitInstance класса, производного от CWinApp (CAutoProg), вызываются соответственно при запуске и завершении программы, поэтому в них удобно производить инициализацию и выполнять завершающие действия. В InitInstance мы создаем окно и выводим его на экран в свернутом виде. Окно создается исключительно для того, чтобы приложение продолжало существовать до тех пор, пока кто-нибудь не закроет его или пока оно не завершится само (это произойдет, когда не останется ни одного указателя на какой-либо из его интерфейсов). ExitInstance удаляет оконный объект, созданный в C++. Эта программа, как и все остальные приложения этой книги (не являющиеся элементами), была создана в среде Visual++ версии 4.x. Я намеренно исключил из этого издания книги поддержку 16-разрядных версий программ (хотя ценой небольших усилий можно было заставить их работать и в 16разрядном режиме). Благодаря тому, что программы стали исключительно 32разрядными, мне удалось воспользоваться некоторыми полезными возможностями Win32 — например, правильной обработкой исключений C++ (более подробная информация приведена в приложении А). Обратите внимание на то, что вместо макросов MFC TRY, CATCH и END_CATCH используются ключевые слова C++ try и catch. По сравнению с первым изданием книги программы стали более компактными, к тому же это улучшило их переносимость между различными компиляторами. Заголовочный файл класса для объекта Automation, который называется AutoProg, приведен в листинге 3-1. Файл реализации AutoProg приведен в листинге 3-2.
ЗАМЕЧАНИЕ Исходный текст и make-файлы программы AutoProg находятся в каталоге \CODE\ CHAP03\ AUTOPROG на сопроводительном диске CD-ROM.
Листинг 3-1. Заголовочный файл класса AutoProg, AUTOPROG.H
} STDMETHODIMP CAutoDisp::put_Salary(long lSalary) { m_lSalary = lSalary; return NO_ERROR; } STDMETHODIMP CAutoDisp::get_Salary(long *lSalary) { *lSalary = m_lSalary; return NO_ERROR; } STDMETHODIMP CAutoDisp::Payraise(long lSalaryIncrement) { m_lSalary += lSalaryIncrement; return NO_ERROR; } Большая часть этого кода представляет собой реализацию объекта Automation. Именно она интересует нас в первую очередь. Для начала посмотрим на заголовочный файл. Как было сказано выше, в нем определяются три класса: CAutoProg, CАutoCF и CAutoDisp. CAutoProg — класс приложения, производный от CWinApp. Из листинга видно, что помимо функций инициализации и завершения CAutoProg содержит функцию CreateClassFactory (о ней речь пойдет в разделе «Создание объекта фабрики класса», стр. 107) и функцию RegisterTypeLibrary (о ней также будет рассказано ниже, хотя о библиотеках типов подробно рассказывается лишь после описания программы). Класс CAutoCF реализует интерфейс IClassFactory для нашего объекта Automation, поэтому он является производным от IClassFactory. Обратите внимание на то, что пять его функций соответствуют методам IClassFactory. В объявление каждой из этих функций входит макрос из заголовочных файлов OLE: STDMETHOD или STDMETHOD_. Оба макроса определяют способ передачи параметров (в настоящее время pascal для Win16 и stdcall для Win32), однако первый определяет функцию, которая возвращает значение HRESULT (как и большинство функций, входящих в ActiveX и OLE API), а второй — функцию, тип возвращаемого значения которой передается в качестве первого параметра. Следовательно, строка
STDMETHOD (QueryInterface)(REFIID riid, void **ppv); объявляет функцию QueryInterface, которая возвращает значение типа HRESULT и получает два параметра, REFIID и void **, а строка
STDMETHOD_ (ULONG, AddRef)(void); объявляет функцию AddRef, которая возвращает значение типа ULONG (объявленное где-то как длинное целое без знака) и не получает никаких параметров. Использование этих макросов существенно облегчает перенос кода между различными платформами, поддерживающими COM и ActiveX. CAutoCF также содержит конструктор, вся работа которого сводится к инициализации двух закрытых переменных, m_ulRefs и m_pAuto. Первая переменная используется как счетчик ссылок для интерфейса фабрики класса (обратите внимание на то, что ей присваивается значение 1), а вторая содержит указатель на объект, создаваемый этой фабрикой класса.
3.3 Реализация интерфейса диспетчеризации с делегированием Несколько больший интерес представляет определение класса CAutoDisp. Этот класс раскрывает свойства и методы объекта через интерфейс IDispatch
www.books-shop.com
и поэтому является производным от IDispatch. Помимо семи методов интерфейсов IUnknown и IDispatch, в него входят и методы, раскрываемые «двойственной» частью интерфейса для реализации функций чтения и записи свойства Salary, а также метод Payraise. Метод, предназначенный для записи свойства Salary (то есть для обновления значения, хранящегося в объекте), называется put_Salary, а метод для получения текущего значения свойства от объекта — get_Salary. Имена методов интерфейса диспетчеризации не предоставляются самим объектом — они берутся из библиотеки типов, содержимое которой может изменяться. Для метода Payraise используется то же самое имя, Payraise. В составе класса также имеется переменная для подсчета ссылок, m_ulRefs, и другая переменная, m_lSalary, в которой хранится значение свойства Salary. В конструкторе класса, также объявленном в заголовочном файле, обеим переменным присваивается значение 0. Итак, контроллеры, умеющие работать с двойственными интерфейсами, будут вызывать эти методы напрямую. Обратите внимание на то, что метод чтения получает указатель, по которому хранится извлекаемое значение, однако работа с указателем скрыта клиентским кодом (примером клиента может послужить Visual Basic):
value = x.Salary Причина заключается в том, что все методы, находящиеся за пределами той части двойственного интерфейса, которая связана с IDispatch, должны возвращать значение типа HRESULT. «Настоящее» возвращаемое значение должно передаваться в виде последнего параметра при вызове метода, и этот параметр должен быть указателем. Как мы вскоре увидим, такое возвращаемое значение должно быть помечено в библиотеке типов ключевым словом retval. Если вызванный метод двойственного интерфейса возвращает исключение, то вместо HRESULT он должен иметь тип DISP_E_EXCEPTION. Затем вызывающий фрагмент может опросить другие интерфейсы объекта (не реализованные в нашем примере), чтобы получить полную информацию об исключении.
3.3.1 Идентификаторы диспетчеризации и свойство значения При вызове методов и свойств через IDispatch (посредством метода Invoke) не используются имена, находящиеся в библиотеке типов. Вместо этого применяются целочисленные идентификаторы диспетчеризации, которые чаще сокращенно называются dispid. Когда те же самые методы и свойства вызываются через двойственный интерфейс, дело обходится без dispid, поскольку функции вызываются напрямую через v-таблицу интерфейса. Тем не менее некоторые dispid оказываются полезными в обоих случаях, так как они обладают самостоятельным семантическим значением независимо от способа обращения к методам или свойствам. Самый важный из этих «специальных» dispid — 0, или DISPID_VALUE в символьной записи. Если свойство (это не относится к методам!) обладает этим dispid, то оно является «свойством значения». Подобным образом можно пометить лишь одно свойство интерфейса. В нашем примере этот dispid был присвоен свойству Salary, это было сделано в библиотеке типов. Возможно, свойство значения лучше рассматривать как свойство по умолчанию, поскольку фрагмент следующего вида:
MsgBox x,
www.books-shop.com
в нашем случае равносилен следующему:
MsgBox x.Salary. Свойства значения также позволяют «преобразовывать» указатель на интерфейс IDispatch объекта в «значение» объекта, причем значение объекта определяется как свойство, помеченное подобным образом.
3.4 Реализации классов При рассмотрении основной части кода бросаются в глаза два макроса DEFINE_GUID, параметрами которых является длинная цепочка чисел. Именно так следует определять и объявлять GUID в ваших программах. DEFINE_GUID либо объявляет переменную, либо, при включении файла INITGUID.H, определяет ее (то есть выделяет под нее место и инициализирует определенным значением). В нашем случае я определяю переменную с именем CLSID_CAuto со значением GUID {AEE97356-B61411CD-92B4-08002B291EED}. Кроме того, я определяю другую переменную IID_IAuto, со значением {4D9FFA39-B732-11CD-92B4-08002B291EED}. Эти GUID были сгенерированы при помощи утилиты GuidGen. Она помещает GUID в буфер обмена (clipboard) как в виде, приведенном в комментарии из верхней части листинга 3.2, так и в виде вызова DEFINE_GUID. Следует помнить, что значения GUID являются уникальными, соответственно, вы можете спокойно пользоваться теми же, сгенерированными мной значениями и не опасаться конфликтов с другими программами (если только кто-нибудь не возьмет эти же GUID и не использует их для других объектов — делать это не рекомендуется). Класс CLSID_CAuto представляет идентификатор класса (CLSID) объекта Automation, а IID_IAuto — идентификатор интерфейса (IID) для двойственного интерфейса. Функция CAutoProg::InitInstance вызывается при запуске программы. Она начинает свою работу с инициализации OLE функцией OleInitialize. Эта функция вызывается всего с одним параметром, указателем на реализацию IMalloc. Интерфейс IMalloc используется для распределения памяти в OLE, причем в 16-разрядном OLE можно (при желании) определить свою собственную реализацию этого интерфейса. Передавая этой функции NULL, вы говорите: «Нет, спасибо, давайте поручим все хлопоты OLE». С появлением 32-разрядного OLE в Windows NT 3.5 было решено, что функция OleInitialize и ее COM-аналог CoInitialize должны получать в качестве параметра только NULL. Другими словами, во всех случаях используется реализация IMalloc, принадлежащая COM. Основная причина заключается в том, что необходимо обеспечить правильную работу механизма распределения памяти в условиях многопоточной среды. Кроме того, были добавлены дополнительные точки перехвата (hooks), позволяющие программам-отладчикам перехватывать события, связанные с распределением, и обнаруживать возможные утечки памяти. Если инициализация OLE заканчивается неудачно, программа прекращает свою работу. Обратите внимание на то, что переменной m_fOleInitSuccess присваивается значение TRUE или FALSE в зависимости от того, удачно ли прошла инициализация OLE. Это позволяет сопоставить успешному вызову OleInitialize необходимый вызов OleUninitialize (это происходит в ExitInstance). Затем InitInstance вызывает функцию RegisterTypeLibrary, предварительно присвоив другой переменной, m_ptinfo, значение 0. Если вызов RegisterTypeLibrary закончится успешно, эта переменная будет содержать указатель на интерфейс ITypeInfo библиотеки типов объекта. Я сохраняю этот указатель, потому что позднее он понадобится мне в реализации интерфейса диспетчеризации. Далее вызывается функция CreateClassFactory, которая возвращает TRUE в случае успешного создания объекта фабрики класса. Если все прошло нормально, программа создает окно и отображает его в виде значка (icon).
3.5 Регистрация библиотеки типов В функции RegisterTypeLibrary «зашито» предположение о том, что файл библиотеки типов находится в том же каталоге, что и выполняемый файл объекта. Пожалуй, стоило бы наделить эту функцию возможностью поиска файла библиотеки типов в других каталогах. Я же в своей программе ограничиваюсь тем, что получаю имя выполняемого файла при помощи функции Win32 API GetModuleFileName и затем изменяю его расширение с EXE на TLB. Обратите внимание на мое оптимистичное предположение о том, что выполняемый файл имеет расширение EXE (или, по крайней мере, длина расширения равна трем символам). Поскольку все функции API 32разрядного COM, работающие со строками, получают и возвращают их в кодировке Unicode, я преобразую полученное имя в Unicode при помощи стандартной функции Win32 API MultiByteToWideChar. Затем имя передается функции API LoadTypeLib, которая, как можно догадаться по ее имени, загружает библиотеку типов и возвращает указатель на ее интерфейс ITypeLib. Этот указатель используется в вызове RegisterTypeLib, в котором происходит фактическое обновление системного реестра по ключу \HKEY_CLASSES_ROOT\TypeLib необходимой информацией. Наконец, поскольку нашей программе еще понадобится указатель на интерфейс ITypeInfo двойственного интерфейса, она вызывает ITypeLib:: GetTypeInfoOfGuid с IID_IAuto и сохраняет полученный указатель в переменной m_ptinfo. Позднее мы еще обратимся к исходному тексту библиотеки типов и к тому, что в нем происходит. А пока вернемся к нашей программе.
3.6 Создание объекта фабрики класса Функция CAutoProg::CreateClassFactory прежде всего создает экземпляр класса CAutoCF при помощи ключевого слова new, пользуясь механизмом обработки исключений C++ (try-catch) для перехвата ошибок, связанных с распределением памяти, поэтому при нехватке памяти для создания объекта CreateClassFactory возвращает значение FALSE. Если экземпляр CAutoCF был создан успешно, программа сохраняет указатель на него в переменной m_pAutoCF, а затем вызывает функцию CoRegisterClassObject для того, чтобы сообщить COM о существовании класса CLSID_CAuto. Обратите внимание на флаги, передаваемые CoRegisterClassObject: CLSCTX_LOCAL_SERVER говорит о том, что фабрика класса реализована в локальном выполняемом файле, а REGCLS_SINGLEUSE — о том, что фабрика класса допускает только одно подключение к ней. В нашем тривиальном случае можно было с таким же успехом использовать флаг REGCLS_MULTIPLEUSE. Далее я освобождаю указатель на интерфейс фабрики класса и в зависимости от обстоятельств возвращаю код успеха (TRUE) или неудачи (FALSE). Деструктор CAutoCF просто вызывает CoRevokeClassObject для того, чтобы уравновесить предшествующий ему вызов CoRegisterClassobject. На самом деле перед вызовом CoRevokeClassObject не мешало бы проверить, что объект класса был успешно зарегистрирован, но я этого не делаю — ради упрощения программы (как известно, любую программу можно в той или иной степени упростить). Реализации CAutoCF::AddRef, CAutoCF::Release и CautoCF::QueryInterface выглядят вполне традиционно. Release удаляет объект фабрики класса при обнулении счетчика ссылок, а QueryInterface вызывает AddRef и возвращает указатель на объект фабрики класса, если запрос касается интерфейса IUnknown или IClassFactory. На любой другой запрос возвращается E_NOINTERFACE. Функция CAutoCF::CreateInstance должна создавать объект, который обладает требуемым интерфейсом (переданным в качестве параметра); для этого она создает новый экземпляр CAutoDisp. Исключения также обрабатываются «родными» средствами C++; единственное исключение, которое я здесь рассматриваю, — нехватка памяти. Затем вызывается принадлежащая объекту реализация QueryInterface для того, что-
www.books-shop.com
бы получить интерфейсный указатель. Обратите внимание на то, что функция CreateInstance сначала проверяет, существует ли объект; какая-нибудь другая реализация могла бы создавать новый объект при каждом вызове CreateInstance. По крайней мере, что у вас имеется свобода выбора. Кроме того, в этой функции следовало бы проверить, равен ли NULL первый параметр, указатель на внешний IUnknown — наш объект не поддерживает объединения. Для простоты я также опускаю соответствующий фрагмент.
ДЛЯ ВАШЕГО СВЕДЕНИЯ В определении CreateInstance отсутствует имя первого параметра (в отличие от всех остальных), поскольку в этой функции он не используется. Хорошо известный трюк из C++: если написанная вами функция не пользуется некоторыми из переданных параметров, можно выкинуть из объявления их имена и избежать предупреждений от компилятора о неиспользованных параметрах. Разумеется, при этом все равно необходимо указать тип каждого параметра.
Функция LockServer завершает реализацию фабрики класса. Из-за простоты моего объекта я игнорирую возможности, предоставляемые этой функцией, и заставляю ее просто передавать код успешного завершения (S_OK). На этом все и кончается. Разумеется, такое поведение не совсем корректно — более того, оно вообще неверно! Мне бы следовало возвращать значение E_NOTIMPL, которое означает, что данная функция не реализована. Я в третий раз оправдаюсь своим стремлением к простоте. Конечно, настоящие приложения должны содержать нормальную реализацию LockServer. Поскольку это уже второе издание книги, а я так и не изменил этот фрагмент, эксперты COM (привет, Крейг!) наверняка будут презирать меня. И все-таки это всего лишь учебная программа… Вот и все, что относится к реализации фабрики класса — как видите, не так уж много.
3.7 Программируемый объект как таковой Объект Automation, создаваемый IUnknown вместе с функциями для чтения и записи и вызова метода этой программой, целиком реализован внутри класса CAutoDisp. Возможно, читатели первого издания еще помнят, что раньше я пользовался вложенным классом C++ для реализации интерфейса, содержащего только методы. Затем этот интерфейс раскрывается через IDispatch, для чего описывающая его информация передается функции Automation API, CreateStdDispatch. Тогда я говорил, что эта функция вместе со структурами данных, использованными для описания интерфейса, устарела и пользоваться ей не следует. К тому же они не позволяли сделать данный интерфейс двойственным. Соответственно, в этом издании я пошел по более современному пути, отказался от архаичной методики и раскрыл интерфейс одновременно и через IDispatch, и через двойственность. Как ни удивительно, это привело к упрощению кода (за счет отказа от вложенных классов), его уменьшению и, разумеется, ускорению работы. Видимо, у прогресса все же есть и положительные стороны. Я хочу рассмотреть реализацию этого интерфейса в обратном порядке — то есть сначала описать функции для доступа к свойствам и вызова метода, а уже потом перейти к составляющим интерфейса, связанным с IDispatch и IUnknown. Как видно из исходного текста, фактическая реализация методов чтения и записи, а также обычного метода выглядит весьма тривиально. Во всех этих случаях ошибки невозможны, поэтому я всегда возвращаю NO_ERROR. Разумеется, в реальной программе функция чтения свойства должна перед
www.books-shop.com
попыткой записи по указателю проверить его и убедиться в том, что он указывает на допустимый объект. put_Salary сохраняет значение переданного параметра в переменной класса m_lSalary, в которой хранится текущее значение свойства. get_Salary получает указатель на область памяти, в которую необходимо скопировать текущее значение — именно это я и делаю. Помните, функция чтения свойства получает указатель потому, что интерфейс является двойственным, и все его методы должны возвращать HRESULT. Реализация метода Payraise также проста — он просто прибавляет переданное значение к текущему значению Salary и на этом завершается. Но довольно о двойственной части. В реализации части, относящейся к IDispatch, встроенные API используются для поручения вызовов через IDispatch методам двойственного интерфейса. Другими словами, попытка узнать значение Salary через IDispatch приведет (в конечном счете) к вызову метода get_Salary. Как это делается? Четыре метода IDispatch (не считая методов IUnknown) выглядят довольно просто, в них используются упомянутые выше волшебные API. GetTypeInfoCount просто устанавливает требуемое значение равным 1 и возвращает NO_ERROR. В нашем случае 1 означает, что объект поддерживает информацию о типах, кроме 1 допускается только значение 0, которое означает, что объект не поддерживает информацию о типах. Что такое «информация о типах»? Пока считайте ее чем-то вроде способа прочитать данные из библиотеки типов объекта, потом станет понятнее. Обратите внимание на то, что параметр LCID из нашей реализации IDispatch:: GetTypeInfo здесь не используется. Идентификатор локального контекста LCID определяет используемый язык (разговорный, а не язык программирования), хотя на самом деле этим его роль не ограничивается. Объект, который предназначен для работы в условиях различных национальных языков (вероятно, к этой категории относится большинство коммерческих приложений и элементов), может раскрывать имена своих свойств и методов на этом языке вместо английского или того языка, на котором вы обычно говорите. Утверждают, будто World Wide Web приведет к отмиранию всех языков, кроме английского — думаю, вряд ли это случится. Для организации многоязыковой поддержки программа может иметь несколько библиотек типов, по одному для каждого поддерживаемого языка. При регистрации библиотеки типов COM выясняет, какой язык ей присвоен, и запоминает эту информацию в реестре. Наша простая программа обходится одним языком и, следовательно, одной библиотекой типов. Если вам захочется поддерживать несколько языков, необходимо воспользоваться параметром LCID для того, чтобы выбрать загружаемую библиотеку типов и вернуть указатель на соответствующий интерфейс ITypeInfo. Кроме того, вам придется видоизменить функцию RegisterTypeLibrary так, чтобы она регистрировала библиотеки типов для всех поддерживаемых языков. В нашем примере я в нескольких местах упростил свою задачу за счет отказа от использования LCID. Далее идет функция GetIDsOfnames. Она предназначена для того, чтобы сопоставить имени конкретный dispid. Кроме того, она устанавливает соответствие между именами параметров и идентификаторами, потому что как параметры, так и сами функции при их вызове через Invoke идентифицируются числовыми значениями. И снова я заставил встроенную функцию DispGetIDsOfNames поработать за себя. Последняя из функций IDispatch — Invoke. Функция Invoke получает великое множество параметров и традиционно представляет собой одну из самых сложных задач при программировании объекта Automation. Поскольку мы не имеем никаких возражений против того, чтобы эта функция просто вызывала соответствующие методы двойственного интерфейса (то есть диспетчеризация попросту выполняется методами двойственного
www.books-shop.com
интерфейса), всю работу Invoke можно свести к вызову функции DispInvoke. Тривиально, не правда ли? И снова обратите внимание на то, что я намеренно выбрал упрощенный путь и проигнорировал параметр LCID. Кроме того, я игнорирую возможность того, что вызванный метод может вернуть исключение — это представляет потенциальную опасность, но сойдет для нашего простого случая. Три последние функции составляют реализацию IUnknown в классе CAutoDisp. Как и в фабрике класса, эти функции устроены достаточно примитивно. QueryInterface возвращает указатель на объект, если запрашивается интерфейс IAuto, IUnknown или IDispatch. Обратите внимание на мою маленькую оптимизацию — предполагая, что большинство запросов будет относиться к двойственному интерфейсу (IAuto), я поставил эту проверку на первое место в операторе if. Если данный случай действительно встречается чаще остальных, проверка ускоряется, поскольку C++ гарантирует, что выражение OR будет вычисляться слева направо. CAutoDisp::Release при обнулении счетчика ссылок перед тем, как удалить себя, направляет окну приложения сообщение о закрытии. Вот и все. Эту программу можно откомпилировать в среде Visual C++ версии 4.0 и выше. Создайте новый проект и выберите в качестве базового типа проекта приложение с поддержкой MFC (не выбирайте приложение, построенное при помощи AppWizard!). Затем включите AUTOPROG.CPP в список файлов проекта. Впрочем, остались еще кое-какие мелочи. Вам также понадобится библиотека типов (если вы скопируете пример с компакт-диска, то она будет включена в проект) и способ сообщить системе о существовании объекта — хотя я включил в программу код для регистрации библиотеки типов, в ней отсутствует код для регистрации самого объекта!
3.8 Регистрация и запуск программы-примера После того как программа будет построена, ее необходимо зарегистрировать. Ниже перечислены ключи и значения, которые требуется добавить в реестр:
HKEY_CLASSES_ROOT\AutoProg.CAuto.2 = AutoProg Server HKEY_CLASSES_ROOT\AutoProg.CAuto.2\CLSID = {AEE97356-B614-11CD-92B4-08002B291EED} HKEY_CLASSES_ROOT\CLSID\{AEE97356-B614-11CD92B4-08002B291EED} = AutoProg Server HKEY_CLASSES_ROOT\CLSID\{AEE97356-B614-11CD-92B408002B291EED}\ProgID = AutoProg.CAuto.2 HKEY_CLASSES_ROOT\CLSID\{AEE97356-B614-11CD-92B408002B291EED}\LocalServer32 = C:\Controls\Chap03\AutoProg\WinDebug\AutoProg.Exe /Automation Первая строка сообщает системе о том, что ProgID AutoProg.CAuto.2 в «человеческой» форме называется AutoProgServer. Вторая строка добавляет в эту категорию ключ, который сообщает системе, какие CLSID раскрывает данный объект. Остальные строки создают дополнительные элементы внутри ключа HKEY_CLASSES_ROOT\CLSID. Первая из них создает ключ для CLSID объекта и снова, исключительно в целях документирования, задает название, понятное для человека. Вторая строка создает ключ ProgID, снова ссылающийся на основную категорию объекта. Две последние строки описывают путь к 32-разрядному EXE-файлу (LocalServer32); проследите за тем, чтобы указанный путь соответствовал местонахождению файла на вашем
www.books-shop.com
программе, но я этого не сделал — разумеется, для упрощения программы). В этом случае сервер может инициализироваться не так, как при обычном запуске — например, он может полностью скрыть свое окно. Обычно информация такого рода заносится в реестр непосредственно самим объектом. Тем не менее в нашем случае можно записать приведенные выше строки в регистрационный файл с расширением REG и добавить в начало этого файла строку, содержащую единственное слово REGEDIT — оно сообщает редактору реестра, что файл действительно содержит информацию для внесения в реестр. В наши дни все большее количество объектов автоматически регистрирует себя в начале работы, а регистрационные файлы считаются неизящным способом обновления реестра.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Ни в одном элементе ActiveX, встречающемся в этой книге, регистрационные файлы не используются.
После того как сервер объекта будет создан и зарегистрирован, можно проверить его, запустив приведенный ранее фрагмент на Visual Basic. Появляется окно сообщения с числом 1234, а затем — следующее окно с числом 1235. После выполнения оператора
Set x = Nothing объект удаляется из памяти (вы можете убедиться в этом с помощью утилиты, выводящей список задач). В соответствующем каталоге на сопроводительном диске CD-ROM можно найти менее тривиальную программу на Visual Basic, которая позволяет глубже заглянуть в тонкости работы Automation. Эта программа содержит экранную форму с двумя кнопками; одна кнопка называется Early/VTable Binding («раннее связывание/связывание через v-таблицу»), а другая — Late Binding («позднее связывание»). Первая кнопка создает объект при помощи оператора
Dim x As New CAuto При этом она пользуется библиотекой типов для проведения раннего связывания, а также осуществляет связывание через v-таблицу, поскольку из библиотеки типов выясняется, что интерфейс на самом деле является двойственным. Вторая кнопка использует более старый механизм связывания:
Dim x As Object Set x = CreateObject ("AutoProg.CAuto.2") Если загрузить проект нашего примера в Visual C++ и установить точки прерывания во всех методах IDispatch и двойственного интерфейса, то вы сможете увидеть, когда и какие функции вызываются — для этого следует запустить объект из Visual C++ и выполнить программу на Visual Basic в интерактивном режиме среды Visual Basic. Вы заметите, что нажатие первой кнопки приводит к непосредственному вызову функции, которая реализует вызываемое свойство или метод. Если нажать вторую кнопку, то перед
www.books-shop.com
из них, пока Invoke доберется до вызова вашего метода), становится очевидно, что вызовы методов двойственного интерфейса работают существенно быстрее. Можно пойти еще дальше и рассмотреть внутрипроцессный сервер, в котором вызов метода двойственного интерфейса преобразуется в единственный вызов через v-таблицу, тогда как вызов через IDispatch требует вызова трех функций, одна из которых достаточно сложна. Кстати говоря, во время работы над этой главой я узнал от Брюса МакКинни (Bruce McKinney), одного из авторов Microsoft Press, и Гленна Хэкни (Glenn Hackney), специализирующегося на технических описаниях Visual Basic, о том, что при раннем связывании и при связывании через v-таблицу можно повысить эффективность работы Visual Basic, если заменить
Dim x As New CAuto на
Dim x As CAuto Set x = New CAuto Дело в том, что в первом случае Visual Basic, встретив x в каком-либо операторе, будет проверять, не равно ли его значение Nothing (то есть значение не присвоено). Зачем? Потому что Visual Basic не может определить порядок выполнения вашего кода (хотя в данном случае сделать это было бы несложно) и потому не может точно определить, в какой момент создается объект для x. Поэтому Visual Basic всегда сначала проверяет x, чтобы узнать, не нужно ли создать объект перед его использованием. Я показал, как реализовать объект Automation на C++ вторым, более сложным способом. Больше всего сложностей на этом пути могла бы вызвать самостоятельная реализация всего интерфейса IDispatch. Чтобы лучше освоиться с Automation, попробуйте немного поиграть с этой программой, добавляя к ней методы и свойства и используя Visual Basic для их тестирования. Кроме того, было бы неплохо попробовать реализовать тот же самый объект на других языках. Вы убедитесь в том, что на некоторых языках эта задача решается намного проще, а на других — намного сложнее. У одних языков выполняемые файлы будут сравнительно большими, у других — маленькими, в одних случаях код будет работать быстрее, в других — медленнее и т. д. В конце концов все равно вам придется выбирать рабочий инструмент. Если знать, на какие компромиссы вам придется пойти, будет проще выбрать оптимальный вариант. Я мог бы наделить эту программу еще одной возможностью, которая должна присутствовать практически в любом дружественном объекте Automation. Речь идет о том, чтобы зарегистрировать «активный» объект, чтобы приложение могло получить интерфейсный указатель на него, пользуясь эквивалентом оператора GetObject из Visual Basic. Для этого необходимо вызвать RegisterActiveObject после того, как интерфейс IDispatch будет готов к работе. При завершении программы следует прекратить регистрацию активного объекта функцией RevokeActiveObject. Функция активного объекта помещает ссылку (обычно моникер) для данного объекта в таблицу рабочих объектов (ROT) — список всех объектов, зарегистрированных подобным образом.
3.9 Подробнее о библиотеках типов
ЗАМЕЧАНИЕ Учтите, что библиотеки типов сейчас обычно пишутся на языке IDL (язык описания интерфейсов), а не на ODL, и что они компилируются утилитой MIDL, а не MkTypeLib. Тем не менее до полного перехода на IDL многие программные инструменты продолжают работать с
www.books-shop.com
ODL и MkTypeLib. Все сказанное ниже относится к библиотекам типов, созданных при помощи как MIDL, так и MkTypeLib (за исключением нестандартных атрибутов, которые поддерживаются только в MIDL).
Чтобы лучше понять, что собой представляет библиотека типов и язык ODL, давайте начнем с уже знакомой нам программы AutoProg. ODL-файл для нее выглядит следующим образом:
[ uuid (4D9FFA38-B732-11CD-92B4-08002B291EED), version(2.0), helpstring("AutoProg Automation Server") ]library AutoProg { importlib("stdole32.tlb"); // Интерфейс диспетчеризации для AutoProg [ uuid(4D9FFA39-B732-11CD-92B4-08002B291EED), helpstring("Automation interface for AutoProg Server"), oleautomation, dual ] interface IAuto : IDispatch { [propput, id(0), helpstring("Sets the current salary")] HRESULT Salary([in] long Salary); [propget, id(0), helpstring("Returns the current salary")] HRESULT Salary([out, retval] long *Salary); [helpstring("Increases the salary")] HRESULT Payraise([in] long Increment); }; //
Первые три строки в совокупности образуют один оператор, объявляющий библиотеку типов. Библиотека называется AutoProg, имеет версию 2.0 (версия 1.0 была в первом издании книги), и с ней связана указанная справочная строка. Эта строка используется в качестве комментария, отображаемого программами для просмотра типов. Также следует обратить внимание на то, что библиотека обладает собственным GUID (который в ODL называется UUID), отличным от GUID самого объекта. Библиотека типов должна иметь собственный GUID, потому что обычно она регистрируется отдельно от объекта (именно это происходит в нашей программе). Первый оператор за фигурной скобкой представляет собой команду для присоединения содержимого другой библиотеки типов, STDOLE32.TLB. Эта стандартная библиотека, которая входит в состав OLE, описывает все системные интерфейсы — такие, как IUnknown и IDispatch. Несколько следующих строк описывают интерфейс нашего объекта. Он обладает собственным UUID и справочной строкой, а также имеет два других «атрибута»: oleautomation и dual. Первый атрибут указывает читателю библиотеки на то, что данный интерфейс совместим с Automation. Другими словами, все методы данного интерфейса возвращают HRESULT, а типы получаемых ими параметров могут использоваться в Automation (Automation ограничивается подмножеством типов, допустимых в C и C++, — можно
www.books-shop.com
считать, что они совпадают с типами, поддерживаемыми в Visual Basic, за исключением «пользовательских типов», приблизительно соответствующих структурам в C). Второй атрибут, dual, сообщает читателю библиотеки, что данный интерфейс является двойственным и, следовательно, связывание для него может осуществляться как через v-таблицу, так и через IDispatch. Именно таким образом Visual Basic узнает о том, что объект CAuto обладает двойственным интерфейсом. Определяемый интерфейс называется IAuto. Он объявляется как производный от IDispatch в следующей строке:
interface IAuto : IDispatch Далее следуют описания трех функций, входящих в него помимо методов IDispatch. Первая из этих функций вызывается, когда пользователь пытается изменить значение свойства Salary. Она объявлена как функция записи свойства с атрибутом propput и dispid 0 (DISPID_VALUE) — на последнее обстоятельство указывает id(0). Последний атрибут представляет собой справочную строку, отображаемую при просмотре функции при помощи служебных программ. Затем следует объявление функции, из которого видно, что функция возвращает значение HRESULT (необходимое условие для того, чтобы функция соответствовала атрибутам интерфейса oleautomation и dual). Здесь наша функция называется Salary (хотя в тексте программы она именуется put_ Salary — окружающий мир видит имя из библиотеки типов) и получает один параметр — длинное целое, которое также называется Salary и используется только «во внутреннем направлении» ([in]). Атрибут [in] сообщает маршалеру (фрагменту кода, который занимается передачей данных между процессами и компьютерами через RPC), что маршалинг данного параметра является односторонним и он не будет снова использоваться в вызывающем фрагменте. Данный атрибут также может иметь значение [out], смысл которого противоположен [in], и [in, out], который указывает на двустороннее использование параметра и, следовательно, на его двусторонний маршалинг. Не стоит и говорить о том, что злоупотребление атрибутом [in, out] замедляет вызов функции. Объявление следующей функции, предназначенной для чтения свойства Salary, очень похоже на предыдущее, за исключением того, что оно помечено атрибутом propget, а его параметр, указатель на длинное целое, имеет атрибут [out], так как это значение «выдается функцией наружу». Ей также присвоен dispid 0, поскольку она работает с тем же свойством, что и функция записи. Наконец, обратите внимание на то, что ее единственный параметр имеет атрибут retval, который сообщает пользователю функции, что данный параметр должен рассматриваться клиентом Automation как возвращаемое значение. Атрибутом retval может быть помечен лишь один параметр; он всегда должен стоять последним в списке параметров функции. Наконец, следует описание метода Payraise. Поскольку этот метод не связан напрямую ни с каким свойством, он не нуждается ни в каких атрибутах, кроме справочной строки. При желании ему можно присвоить конкретный dispid, однако по умолчанию он получит dispid 1 (поскольку предыдущая функция имеет dispid 0). Метод получает один параметр, Increment, который представляет собой длинное целое. Следующая часть библиотеки типов определяет сам объект COM. В ODL он носит название «вспомогательного класса» (coclass). Он также обладает GUID, но на этот раз его значение совпадает с GUID, созданным для CLSID объекта. С ним также связана справочная строка. Я решил назвать объект CAuto — по аналогии с IAuto для интерфейса диспетчеризации (на самом деле вам предоставляется полная свобода в выборе имен). Я определил этот класс как состоящий из интерфейса IAuto, который также помечен как интерфейс по умолчанию ([default]), и интерфейса IDispatch, поскольку он раскрывает оба этих интерфейса. Обратите внимание на то, что для
www.books-shop.com
вспомогательного класса был задан атрибут appobject. Он сообщает читателю библиотеки типов о том, что доступ к членам вспомогательного класса может осуществляться без уточнения (то есть без получения дополнительной информации об имени). Данный атрибут является необязательным, однако его стоит использовать во всех вспомогательных классах верхнего уровня. На этом завершается наше знакомство с исходным текстом библиотеки типов. Если откомпилировать ее при помощи MIDL или MkTypeLib, то полученную библиотеку можно просмотреть при помощи утилиты, подобной Ole2vw32 (то есть «просмотр объектов OLE 2», OLE 2 Object View), которая входит в состав Visual C++ версий 2.0 и выше (рис. 3-1). После того как исходный текст будет скомпилирован и превратится в библиотеку типов, ею можно пользоваться как источником информации об интерфейсах, свойствах и методах объекта. Обычно библиотека типов включается в состав ресурсов приложения в качестве двоичного ресурса (для DLL) или же остается в виде автономного файла (для EXE) и регистрируется в реестре Windows. Чтобы воспользоваться библиотекой типов, приложения могут вызвать функции для получения указателей на интерфейсы библиотеки (такие, как ITypeInfo) и через интерфейсный указатель воспользоваться функциями для просмотра нужной информации. Библиотеки типов могут содержать и другую полезную информацию, в том числе и определения типов и других интерфейсов. С выходом Windows NT 4.0 появился новый интерфейс для работы с библиотекой типов, ITypeInfo2. Он позволяет создавать нестандартные атрибуты (атрибутами называются строки внутри квадратных скобок), которые могут читаться пользователями вашего объекта. Нестандартный атрибут фактически представляет собой GUID, с которым связано некоторое значение.
Рис. 3-1.Содержимое библиотеки типов при просмотре утилитой Ole2vw32 из Microsoft Visual C++ версий 2.0 и выше
3.10 Возвращаемся к структурированному хранению В этом разделе я на примере нашей программы AutoProg продемонстрирую работу механизма структурированного хранения, о котором говорилось в предыдущей главе. Я не буду наделять наш объект полноценными возможностями структурированного хранения (например, реализовывать
www.books-shop.com
IPersistStorage), поскольку это приведет к чрезмерному усложнению программы (простота превыше всего!). Вместо этого я собираюсь включить в AutoProg всего один метод Automation, который называется Store и создает в корневом каталоге файл TEST.AUT. В этом файле-хранилище метод создает поток с именем MyStream и сохраняет в нем текущее значение свойства Salary, которое сохраняется как текстовая строка вида Salary = значение.
ЗАМЕЧАНИЕ Исходный текст и make-файлы обновленной программы AutoProg находятся в каталоге \CODE\CHAP03\AUTOPRO2 на сопроводительном диске CD-ROM.
По сравнению с предыдущей версией программы произошли следующие изменения: в файле AUTOPROG.H, сразу же после объявления функции Payraise, я добавил строку:
STDMETHOD (Store)(void); В ней объявляется новая функция Store. Изменения в основном файле программы AUTOPROG.CPP выглядят столь же прямолинейно. В конце файла появилась новая функция:
В этом фрагменте структурированное хранилище создается функцией OLE API StgCreateDocFile (doc-файл — старое имя, которым раньше в Microsoft назывались файлы структурированного хранения; оно вышло за пределы фирмы и получило распространение среди других разработчиков. Сейчас его использование считается нежелательным). Функции передаются следующие флаги:
STGM_CREATE — сообщает функции о необходимости перезаписи существующего файла с таким же именем; STGM_SHARE_EXCLUSIVE — сообщает, что файл открывается в режиме монопольного доступа; STGM_READWRITE — сообщает, что доступ к файлу будет осуществляться по чтению/записи.
Отсутствие флага для режима транзакции означает, что я собираюсь пользоваться непосредственным режимом.
ДЛЯ ВАШЕГО СВЕДЕНИЯ В документации почему-то отсутствует четкое упоминание о том, что во всех функциях для создания хранилищ и потоков необходим флаг, определяющий режим доступа — такой, как STGM_SHARE_EXCLUSIVE. Признаюсь, это вызвало у меня определенные трудности!
Обратите внимание на то, что имя создаваемого файла должно передаваться функции в виде строки в кодировке Unicode, для этого используется стандартный конструктор:
L"String" После открытия потока вызывается функция IStorage::CreateStream, которая создает поток MyStream (и снова строка передается COM в кодировке Unicode) внутри хранилища, возвращаемого функцией StgCreateDocFile. Если создание потока прошло успешно, выводимая строка строится функцией wsprintf и выводится в поток методом IStream::Write. Затем функция закрепляет запись и перед тем, как завершить работу, сначала освобождает указатель IStream, а вслед за ним — указатель IStorage. Хотя IStream::Commit ничего не делает в текущей версии OLE, в будущем этот метод может быть наделен полезными функциями, так что правила хорошего тона требуют вызвать его. Чтобы расширить нашу программу на Visual Basic для тестирования нового метода Store, достаточно добавить одну строку перед освобождением объекта: x.Store
ЗАМЕЧАНИЕ Проследите за тем, чтобы элемент реестра LocalServer32 для AutoProg ссылался на обновленный вариант выполняемого файла.
www.books-shop.com
Теперь можно просмотреть и проверить содержимое созданного файла структурированного хранения при помощи утилиты DfView (DocFile Viewer), входящей в состав Win32 SDK.
3.11 Архитектура элементов ActiveX
ЗАМЕЧАНИЕ С момента первого издания этой книги архитектура элементов несколько изменилась. Теперь элемент определяется как любой COM-объект, поддерживающий хотя бы интерфейс IUnknown. Таким образом, элементы перестали быть какой-то особой разновидностью COMобъектов. Тем не менее большинство элементов все же предлагает своим пользователям более широкий набор возможностей, поскольку элемент, реализующий только интерфейс IUnknown, вряд ли принесет особую пользу. В первой части этого раздела описывается первоначальный вариант архитектуры Элементов OLE, известный в Microsoft под названием «Элементов OLE 94». Вторая часть развивает тему и описывает новую архитектуру «Элементов OLE 96». Разумеется, с того времени Microsoft успела разработать архитектуру Элементов ActiveX, которая включает все, упомянутое выше, а также содержит ряд новых положений, имеющих особое значение для Internet. Я не стану подробно рассматривать ее, поскольку в книге имеется специальная глава, целиком посвященная этой теме (глава 13).
Поскольку теперь от элемента требуется лишь реализация IUnknown, многие возможности элементов могут отсутствовать. По этой причине желательно, чтобы элементы пользовались новыми компонентными категориями COM для того, чтобы сообщить контейнеру о своих возможностях. Элементы также могут выбрать, какому варианту архитектуры они желают соответствовать — Элементам OLE 94, Элементам OLE 96, Элементам ActiveX или их произвольному сочетанию!
На самом деле элементы ActiveX не так уж сильно отличаются от других COMобъектов: они реализуют одни интерфейсы и пользуются услугами других. Элемент ActiveX, который соответствует архитектуре Элементов OLE 94, должен как минимум обладать способностью к внутренней активизации на месте и поддерживать Automation, допуская чтение/запись своих свойств и вызов методов. Хотя элементы ActiveX можно реализовать в виде EXE-файлов (то есть локальных серверов), все же нетрудно предвидеть, что большинство их будет создаваться в виде DLL-библиотек (то есть внутрипроцессных серверов). Разумеется, все элементы, создаваемые в данной книге, относятся ко второй категории. От элементов ActiveX часто требуется выполнение некоторых задач, которые не поддерживаются стандартными COM-интерфейсами, — например, способностью инициировать события, осуществлять привязку к источникам данных и поддерживать лицензирование. Соответственно, архитектура ActiveX добавляет ряд новых положений к уже существующим стандартам. Здравый смысл подсказывает, что настало время представить некоторые термины, связанные с элементами ActiveX. Однако дать сейчас полные определения было бы затруднительно, так что прошу учесть, что «определения» из приведенной ниже таблицы будут уточняться в последующих главах.
Термин
Значение
Событие (Event)
Когда элемент должен сообщить контейнеру: «Произошло нечто важное», он «инициирует» событие. Другими словами, событие представляет собой асинхронное оповещение, посылаемое от элемента к контейнеру, а его семантика определяется
www.books-shop.com
элементом. Существует целый ряд стандартных событий; о них будет рассказано в главе 8, «События». Контейнер может поддерживать собственный набор общих переменных, значения которых могут просматриваться элементами при загрузке. Например, контейнер может задать цвет фона или шрифт по Свойство окружения умолчанию. Элемент, обладающий соответствующими (Ambient Property) свойствами, решает, следует ли ему прочитать и использовать свойства окружения (строго говоря, свойства окружения раскрываются не контейнером, а клиентским узлом, подробности приведены ниже).
Расширенное свойство(Extended Property)
Некоторые свойства, которые пользователь может ассоциировать с элементом (скажем, его размер и положение), на самом деле определяются контейнером, а не элементом. Обычно элемент не выбирает своего положения на экранной форме. Вместо этого он позволяет расположить себя там, где сочтет нужным разработчик. Свойства, которые не раскрываются объектом, но тем не менее логически связаны с ним, называются «расширенными». Они раскрываются для контейнера (словно обычные свойства) при помощи «расширенных элементов». Контейнер также может реализовать расширенные методы и события.
В последующих разделах настоящей главы кратко рассматриваются базовые принципы, на которых основана каждая из этих концепций; я не стараюсь чрезмерно углубляться в подробности, поскольку многим из этих тем вкниге посвящены целые главы. Первое, на чем мы остановимся, — каким образом поведение элементов может быть запрограммировано пользователем контейнера, в который они внедрены. Затем мы перейдем к рассмотрению свойств окружения, событий и других средств общения элемента с контейнером; стандартных типов, определенных элементами ActiveX или для них; устойчивости свойств; лицензированию; версиям и другим (порой весьма хитроумным) аспектам этой технологии!
ЗАМЕЧАНИЕ Как было сказано в замечании на первой странице этой главы, для создания полезных элементов совершенно не обязательно читать и понимать материалы этой главы, хотя изложенные в ней сведения могут пригодиться при выполнении более сложных или нетривиальных операций с элементом или контейнером. Вы можете на свое усмотрение продолжать чтение этой главы или перейти к главе 4.
3.12 Языковая интеграция Поведение элементов ActiveX можно программировать — в этом заключается одно из их отличий от обычных внедренных объектов. Эта способность к программированию осуществляется средствами Automation, и потому контейнер, содержащий внедренные элементы, должен предоставить язык программирования для управления ими. Так, приложения Microsoft содержат различные диалекты Visual Basic (скажем, Visual Basic for Applications или Visual Basic Script); в Visual C++ предусмотрен механизм для программирования элементов из C++. Способ интеграции языка программирования с контейнером никак не влияет на работу элементов — другими словами, для элементов совершенно неважно, каким образом пользователь программирует их, если это делается средствами Automation.
www.books-shop.com
Тем не менее средства языка должны поддерживать чтение и запись свойств, вызов методов и обработку событий. Все эти возможности имеются в Visual Basic. Ниже показано, как организована интеграция Visual Basic с элементом ActiveX. Предполагается, что MyControl — переменная Visual Basic типа Object (или вспомогательного класса, поддерживающего Automation), которая ссылается на работающий элемент с именем TheControl1, находящийся на форме:
Запись свойства MyControl.TheProperty = 12 Чтение свойства x% = MyControl.TheProperty Вызов метода If MyControl.TheMethod = True Then ... Обработка событий Sub TheControl1_TheEvent (params) ... В разделе «Документы ActiveX» прошлой главы упоминалась концепция «клиентского узла» — специального объекта, реализованного контейнером и представляющего собой «шлюз» между контейнером и внедренным объектом. Элементы ActiveX могут пользоваться клиентским узлом, поскольку они также могут выступать в роли внедренных объектов. Вдобавок контейнер иногда реализует параллельный объект, который называется «расширенным объектом». Он создается посредством объединения с основным элементом — следовательно, элементы должны обладать средствами для организации объединения. Во всех элементах, созданных при помощи MFC, присутствует поддержка объединения, хотя, вообще говоря, программист может написать элемент, не обладающий способностью к объединению. В этом случае программист также должен позаботиться о том, чтобы создание элемента завершалось неудачно в случае, если создающий его контейнер также попытается осуществить объединение. Другие средства для разработки элементов (например, ActiveX Template Library (ATL) или шаблон Win32 BaseCtl) позволяют принять решение относительно возможности объединения на стадии проектирования элемента. Объединенный объект воспринимается языком программирования контейнера так, как если бы он был элементом: его свойства и методы, а также расширенные свойства (и методы, если они существуют) раскрываются через один общий интерфейс Automation. В частности, объединение может использоваться для слияния интерфейсов двух объектов: интерфейс Automation расширенного элемента немедленно передает интерфейсу основного объекта все вызовы свойств или методов, которые он не распознает. Приложения, которые хотят поддерживать внедрение элементов ActiveX и пользоваться передовыми возможностями элементов (например, событиями и связыванием данных), должны делать нечто большее, чем стандартные контейнеры для внедрения объектов. Контейнеры старого образца (скажем, Excel 5.0) допускают внедрение элементов и даже позволяют программировать их средствами Automation, однако они не умеют реагировать на некоторые полезные вещи, которые могут делать эти элементы. Конечно, некоторые старые контейнеры не разрешают внутренней активизации объектов и ограничивают их внешней активизацией. На рис. 32 показаны интерфейсы, которые часто раскрываются элементами ActiveX, соответствующими архитектуре Элементов OLE 94. Также на нем перечислены интерфейсы,которые должны быть реализованы контейнером для поддержки элементов OLE. Эти интерфейсы будут описаны в оставшейся части главы вместе с другими аспектами архитектуры Элементов ActiveX.
www.books-shop.com
Рис. 3-2.Интерфейсы элементов и контейнеров, отвечающих спецификации Элементов OLE 94
3.13 Свойства окружения «Свойствами окружения» называются свойства, поддерживаемые контейнером для того, чтобы внедренные элементы могли прочитать и использовать их значения. Наличие свойств окружения у контейнера ни в коем случае не является обязательным, однако они могут сильно пригодиться для обеспечения более тесной интеграции. Элемент не может присвоить значение таким свойствам, поэтому можно считать их доступными только для чтения. Обычно контейнер, предоставляющий свойства окружения, также предоставляет механизм для их изменения пользователем. Бесспорно, контейнер, не обладающий таким механизмом, принесет меньше пользы, чем тот, в котором этот механизм предусмотрен! В число наиболее распространенных свойств окружения обычно входят цвета фона и текста, шрифты и размеры страниц.
Стандартные свойства окружения В спецификации Элементов ActiveX приведен список стандартных свойств окружения; мы подробно рассмотрим их в главе 5, «Свойства». А пока вам необходимо знать лишь то, что свойства окружения не ограничиваются списком, содержащимся в спецификации, и что элемент должен заранее знать о существовании свойства окружения для того, чтобы разумно воспользоваться им. Для вас как разработчика элементов это означает, что вы должны заранее знать, какие свойства окружения будут использоваться вашими элементами. Каждому стандартному свойству окружения помимо имени присваивается определенный dispid. Важно помнить, что полагаться следует именно на значение dispid, а не на имя. Следовательно, не стоит предполагать, что свойство окружения для цвета фона обязательно будет называться BackColor; вместо этого следует пользоваться его стандартным dispid: –701 (да, все правильно — отрицательное число). Имена свойств могут изменяться в процессе локализации, так что на французском или немецком английские слова могут иметь совершенно иное значение. Разумеется, вместо излишне конкретного и потому опасного –701 следует пользоваться константой DISPID_AMBIENT_BACKCOLOR из файла OLECTL.H. Свойствам окружения, выходящим за пределы стандартного списка, следует присваивать положительные значения dispid. Если контейнер поддерживает какое-либо стандартное свойство окружения, то он должен реализовать его общепринятым способом, не пытаясь наделить его каким-то новым смыслом. Свойства окружения предоставляются контейнером, и только контейнер может решить, какие из этих свойств будут реализованы (причем их список может изменяться для различных узлов контейнера). По этой причине трудно представить себе контейнер, который бы реализовал для свойств окружения двойственный интерфейс. В этом случае кому-нибудь пришлось бы определить двойственный интерфейс для свойств окружения, который
www.books-shop.com
пришлось бы реализовать всем — но это привело бы к перегрузке всех контейнеров избыточным кодом для редко используемых возможностей. Элемент может быть спроектирован в расчете на работу в определенном контейнере и, следовательно, на свойства окружения, предоставляемые лишь этим контейнером. Для такого элемента необходимо предусмотреть ситуацию, при которой обращение к этим свойствам завершается неудачей, поскольку он может быть внедрен в другой контейнер. Если элемент действительно работает только в одном контейнере, он не должен допускать, чтобы его внедряли еще куда-то. Два стандартных свойства окружения могут применяться элементами для выбора действий, которые они должны предпринять. Свойство UserMode (DISPID_ AMBIENT_USERMODE) используется контейнерами, поддерживающими два рабочих режима, — примером могут послужить режимы конструирования и выполнения в Visual Basic. В одном из этих режимов элемент при активизации может выполнить какие-то особые действия или же отобразить себя каким-то нестандартным образом. Данное свойство имеет логический тип, так что его значение равно TRUE (в режиме выполнения) или FALSE (в режиме конструирования). Свойство UIDead (DISPID_AMBIENT_UIDEAD) также является логическим, оно позволяет элементу определить, должен ли он сейчас реагировать на операции пользовательского интерфейса. Например, если во время сеанса отладки программа останавливается на точке прерывания, контейнер может присвоить UIDead значение TRUE — тем самым он сообщает всем элементам о том, что обработка ввода от пользователя временно прекращена. Любой уважающий себя элемент должен соблюдать это правило. Два других стандартных свойства окружения, ShowGrabHandles< и Show Hatching, также используются в режимах работы контейнера. Если внедренный объект является UI-активным (соответствующая поддержка может быть предусмотрена в большинстве элементов), вокруг него обычно отображаются маркеры для изменения размеров (контейнер предоставляет маркеры лишь для внедренных объектов, активизированных на месте). Тем не менее в таких контейнерах, как Visual Basic, маркеры допустимы лишь в режиме конструирования, а во время выполнения их присутствие нежелательно. Следовательно, при UI-активизации элемент должен узнать значение свойства окружения ShowGrabHandles (DISPID_AMBIENT_SHOWGRABHANDLES) и определить, следует ли ему отображать маркеры. Аналогично, вокруг UI-активного внедренного объекта отображается рамка с косой штриховкой (и снова для внедренных объектов, активизированных на месте, ее рисует контейнер), по присутствию которой можно судить об активности внедренного объекта. В режиме выполнения такая рамка нежелательна, поэтому перед тем, как рисовать ее, элемент должен проверить значение свойства окружения ShowHatching (DISPID_AMBIENT_SHOWHATCHING). Но каким образом элемент определяет значения свойств окружения? Вероятно, вы уже догадались — он пользуется средствами Automation. Клиентский узел раскрывает эти свойства через интерфейс Automation (обратите внимание: именно клиентский узел, а не контейнер!). Следовательно, различные клиентские узлы одного контейнера могут раскрывать разные наборы свойств окружения; например, в электронной таблице каждая ячейка может назначить выбранные для нее шрифт и цвет своими свойствами окружения. Чтобы получить интерфейс Automation для получения свойств окружения, элемент вызывает QueryInterface для любого интерфейса данного узла с идентификатором IID_DISPATCH. Затем он вызывает IDispatch::Invoke для нужного dispid, чтобы получить нужное значение. Если клиентский узел не поддерживает свойства окружения, он возвращает DISP_E_MEMBERNOTFOUND.
3.14 События Обработка событий является довольно интересным дополнением к
www.books-shop.com
стандартной архитектуре COM, поскольку реализацию для нее должен обеспечивать контейнер. Другими словами, генерируемые элементом события описываются самим элементом, но при этом необходимо, чтобы контейнер предоставил интерфейс для их обработки. При помощи событий элемент сообщает контейнеру о выполнении некоторого условия. Что именно произошло, определяет сам элемент. Возбуждение событий происходит асинхронно, а это означает, что контейнер должен быть готов к получению и обработке событий в любой момент времени после внедрения объекта. События реализуются при помощи стандартных методов Automation, однако вся тонкость заключается в том, что реализующий эти методы интерфейс Automation принадлежит контейнеру, а не элементу. Когда элемент хочет возбудить событие, он вызывает соответствующий метод контейнера через предоставленный контейнером двойственный интерфейс или через IDispatch::Invoke (в большинстве существующих контейнеров события реализуются только через IDispatch). Поскольку реализация интерфейса Automation была предоставлена контейнером, элемент в данной ситуации выступает в роли «источника» события, а интерфейс Automation контейнера называется «приемником» (sink). Библиотека типов элемента описывает методы событий, реализация которых необходима элементу, — выглядит это так, словно эти методы реализуются самим элементом. Тем не менее в разделе вспомогательного класса ODL-файла этот интерфейс описывается как источник; это говорит о том, что элемент на самом деле не реализует его. Рассмотрим пример. Допустим, IHexocxEvents — интерфейс диспетчеризации событий в элементе, а IHexocx — интерфейс диспетчеризации для стандартных свойств и методов элемента. В этом случае описание вспомогательного класса будет выглядеть следующим образом:
[ uuid(37D341A5-6B82-101B-A4E3-08002B291EED), helpstring("Hexocx Control")] coclass Hexocx { [default] interface IHexocx; interface IDispatch; [default, source] interface IHexocxEvents;}; В соответствии с этим определением вспомогательный класс Hexocx имеет три интерфейса Automation: IHexocx, сопровождающий его интерфейс IDispatch, и интерфейс IHexocxEvents. IHexocx реализуется самим элементом, а IHexocxEvents — нет. Атрибут default означает, что программирование элемента должно осуществляться через данный интерфейс, если только прямо не указано обратное. Таким образом, для вспомогательного класса IHexocx является «первичным интерфейсом Automation», а IHexocxEvents — «первичным набором событий». Элемент мог бы предоставлять набор расширенных возможностей для опытных пользователей (то есть для нас, правда?) через другой интерфейс (скажем, IHexocxPowerUser). Новый интерфейс не был бы помечен атрибутом default, поэтому никто не смог бы им пользоваться без непосредственного указания его IID. Возникает вопрос: как элемент соединяется с интерфейсом Automation контейнера для работы с событиями? Он не может вызвать QueryInterface для IDispatch клиентского узла, поскольку по определению при этом будет возвращен указатель на интерфейс IDispatch для работы со свойствами окружения. Разумеется, он мог бы запросить IID для двойственного интерфейса. Спецификация Элементов ActiveX решает эту проблему, определяя для COM общий механизм, при помощи которого объект может сказать, что он пользуется услугами данного интерфейса, а не предоставляет их: иначе говоря, он выражает готовность организовать связь с реализацией интерфейса. Этот механизм называется «точкой соединений» (connection point).
www.books-shop.com
3.15 Точки соединения Поддержка событий элементами ActiveX организована при помощи специальных механизмов COM — «точек соединения». «Точка соединения» представляет собой интерфейс, раскрываемый объектом для установления связи с реализацией другого интерфейса, с которым объект хочет «общаться». Интерфейс событий элемента описывается как интерфейс Automation в библиотеке типов, при этом он помечается ключевым словом source. Это означает, что интерфейс не реализуется самим элементом. Элемент лишь предоставляет точку соединения, через которую контейнер может подключить свою реализацию методов Automation для обработки событий.
ЗАМЕЧАНИЕ Важно понимать, что точки соединения являются расширением COM, а их применение не ограничивается элементами ActiveX. Следовательно, любой механизм (в том числе и события), который использует точки соединения, может быть реализован любым COMобъектом. В главе 4 приведена разновидность AutoProg, в которой происходит именно это.
Точку соединения можно легко определить как реализацию интерфейса IConnectionPoint. Контейнер работает с точкой соединения через другой интерфейс, IConnectionPointContainer, который позволяет внешнему объекту просмотреть список точек соединения, поддерживаемых элементом, или же получить конкретную точку соединения по ее «имени» (имя точки соединения обычно определяется как IID интерфейса, с которым она должна поддерживать связь).
Подробнее о точках соединения Работа механизма точек соединения основана на том, что объект получает возможность представить список «точек», с которыми он хотел бы связать реализации некоторых интерфейсов. Этот список ведется при помощи интерфейса IConnectionPointContainer, который содержит следующие методы:
HRESULT EnumConnectionPoints(IEnumConnectionPoints **ppEnum) HRESULT FindConnectionPoint(REFIID iid, IConnectionPoint **ppCP) Метод EnumConnectionPoints возвращает указатель на интерфейс IEnumConnectionPoints. Интерфейсы IEnumxxx, где xxx определяет разновидность объекта, используются в COM для работы с наборами объектов. Все интерфейсы IEnumxxx содержат одни и те же методы: Next, Skip, Reset и Clone. Такие интерфейсы называются «итераторами», поскольку они служат для перебора входящих в набор объектов. Automation содержит обобщенный итератор, IEnumVARIANT, предназначенный для работы с наборами через интерфейс Automation. Фактический тип объектов, входящих в набор, определяется содержимым переменных VARIANT: например, набор может применяться объектом для составления списка текущих открытых документов. Тип VARIANT представляет собой объединение (union), которое может хранить данные самых различных типов, от простых целых до указателей на IDispatch. Итерация в наборах, организованных при помощи IEnumVARIANT, осуществляется в Visual Basic посредством оператора For Each...Next. В нашем случае возвращаемый объект-итератор позволяет работать с набором, состоящим из точек соединения. FindConnectionPoint возвращает конкретную точку соединения по
www.books-shop.com
идентификатору интерфейса, для связи с которым предназначена данная точка. Следовательно, для нахождения точки соединения, предназначенной для связи с реализацией интерфейса IMyInterface, следует передать IID_MYINTERFACE. Идентификатор интерфейса, с которым связывается точка, можно рассматривать как ее «имя». На самом деле объект может иметь сразу несколько точек соединения, предназначенных для связи с одним и тем же интерфейсом. В этом случае вызов FindConnectionPoint завершится неудачей, и для установления связи придется воспользоваться другими приемами — например, перебором списка точек соединения. Если точка соединения предназначена для связи с реализацией IDispatch, а не с обобщенным COM-интерфейсом, методу FindConnectionPoint следует передавать не IID_DISPATCH, а IID интерфейса, раскрываемого через IDispatch. Следовательно, для точки соединения, которая бы позволяла контейнеру реализовывать события, связанные со вспомогательным классом Hexocx из приведенного выше примера, следует передавать IID для IHexocxEvents, а не для IID_DISPATCH. Надеюсь, причина ясна: интерфейс IDispatch является обобщенным и не обладает контрактом, в котором бы перечислялись поддерживаемые им свойства и методы. С другой стороны, конкретная реализация обладает именно этим свойством. IСonnectionPoint содержит следующие методы:
HRESULT GetConnectionInterface(IID *pIID); HRESULT GetConnectionPointContainer(IConnectionPointContainer **ppCPC); HRESULT Advise(IUnknown *pUnkSink, DWORD *pdwCookie); HRESULT Unadvise(DWORD dwCookie); HRESULT EnumConnections(IEnumConnections **ppEnum); Метод GetConnectionInterface возвращает IID интерфейса, с которым желает связываться конкретная точка соединения. GetConnectionPointContainer является «обратным указателем» и позволяет программе добраться до реализации интерфейса IConnectionPointContainer, содержащей данную точку. Advise вызывается стороной, реализующей интерфейс (то есть приемником), для установления связи между реализацией интерфейса и точкой соединения. Обратите внимание: независимо от того, какой интерфейсный указатель передается вызывающей стороной, для получения нужного интерфейсного указателя всегда вызывается QueryInterface. Advise возвращает в pdwCookie специальное значение, которое выступает в роли логического номера (handle) для данной связи. При неудачном вызове Advise оно равно 0. Unadvise разрывает соединение, получая ее логический номер в качестве параметра. Метод EnumConnections представляет больший интерес: он позволяет построить список подключений для данной точки соединения. Да, вы не ошиблись — точка соединения может быть подключена одновременно к несколькими реализациям своего интерфейса. Это обстоятельство открывает возможность «мультиплексирования», при котором точка соединения, например, может послать события сразу нескольким реализациям интерфейса. Код, работающий через точку соединения, должен позаботиться о том, чтобы все вызовы методов связанного интерфейса доходили до всех связанных реализаций. Работа с точками соединения облегчается благодаря двум интерфейсам: IProvideClassInfo и IProvideClassInfo2. Последний интерфейс представляет собой усовершенствованный вариант первого. IProvideClassInfo содержит всего один метод:
HRESULT GetClassInfo(ITypeInfo **ppTI); который возвращает указатель на интерфейс ITypeInfo, описывающий точки соединения объекта. Интерфейс ITypeInfo на самом деле описывает вспомогательный класс в библиотеке типов объекта, так что в нашем
www.books-shop.com
примере с Hexocx он будет описывать интерфейсы IHexocx и IHexocxEvents. Тем не менее последний будет помечен как интерфейс-источник, благодаря чему читатель этой информации поймет, что он должен реализовать данный интерфейс для того, чтобы связаться с соответствующей точкой соединения. Новая версия интерфейса, IProvideClassInfo2, несколько облегчает поиск IID для интерфейса событий. Вместо того чтобы возиться с информацией, возвращаемой GetClassInfo, можно вызвать метод GetGUID, который возвращает GUID, соответствующий запрошенному типу:
HRESULT GetGUID (DWORD dwGuidKind, GUID *pGUID); Параметр dwGuidKind может принимать различные стандартные значения, которые позволяют методу в зависимости от требования возвращать разные GUID. Типичный элемент будет получать запросы на GUIDKIND_DEFAULT_SOURCE_DISP_IID, который относится к интерфейсу Automation для принятого по умолчанию источника (на самом деле пока что это единственная константа GUIDKIND, определенная для IProvideClassInfo2::GetGUID). Интерфейс IProvideClassInfo2 является производным от IProvideClassInfo и содержит только этот один дополнительный метод. Точки соединения не являются привилегией элементов ActiveX и могут использоваться объектами других типов, которые пожелают установить связь с одним или несколькими интерфейсами, предоставленными контейнером. На рис. 3-3 показано, каким образом точки соединения и контейнеры устанавливают связь с реализацией интерфейса, а также продемонстрирована возможность мультиплексирования. Вернемся к нашим событиям. Контейнер должен получить от элемента информацию типа для события и, руководствуясь ею, динамически создать реализацию IDispatch — или, еще лучше, двойственного интерфейса. Затем он передает ее методу Advise соответствующей точки соединения элемента. В дальнейшем все события, возбуждаемые элементом, будут передаваться этой реализации. В общем случае контейнеры, поддерживающие элементы ActiveX, передают эти события программисту, который пишет код для их обработки. Если в пользовательской программе не существует обработчика для того или иного события, контейнер должен обеспечить обработку события по умолчанию. В этом заключается еще один аспект языковой интеграции контейнера, о которой говорилось раньше. Например, Visual Basic позволяет программисту написать процедуру Sub, которая вызывается при каждом возбуждении события. Этой процедуре передаются параметры события, определяемые элементом. Некоторые события являются стандартными и определяются в спецификации Элементов ActiveX, к их числу относится событие Error. Оно сообщает контейнеру о том, что в ходе выполнения программы произошла какая-то ошибка, однако используется только асинхронно. Если ошибка происходитво время вызова метода или свойства, то ошибка возникает синхронно с вызовом, поэтому для того, чтобы сообщить о ней контейнеру, используется стандартный для Automation механизм исключений. Если ошибка происходит в другой момент, элемент должен возбудить событие Error. Событие Error в спецификации Элементов ActiveX, как и исключения Automation, позволяет элементу передать контейнеру полезную информацию — например, сведения о справочном файле и контекстный идентификатор для данной ошибки. Данный механизм позволяет Basic-подобным языкам не только перехватывать ошибку при помощи операторов OnError, но и помогает обеспечить вывод справочной информации.
3.16 Оповещения об изменении свойств В число возможностей нестандартных элементов Visual Basic, которые часто приходится поддерживать элементам ActiveX, входит «связывание данных», то есть установление связи между свойством и источником данных (скажем, столбцом в файле базы данных), чтобы свойство отражало значение данного столбца для определенной записи, а сведения об его изменении поступали обратно в базу данных. Аналогично, связанное свойство может применяться для добавления новых значений в базу. Чаще всего связывание данных используется для элементов, находящихся на экранной форме, поскольку это позволяет упростить отображение информации из источника данных. Например, текстовое поле может использоваться для отображения значения столбца и его редактирования пользователем. Тем не менее ничто не мешает организовать связывание данных и для элемента, не отображаемого на форме.
ЗАМЕЧАНИЕ Я воспользовался обобщенным термином «источник данных», поскольку связывание данных не обязано осуществляться только через реляционную базу данных. По мере развития компьютерных технологий появляется все больше объектов, которые можно рассматривать в качестве источников данных. Разумеется, в их число входят реляционные и индекснопоследовательные базы данных, а также такие разнородные объекты, как файловые системы, транзакции, файлы текстовых редакторов и т. д.
Элементы ActiveX могут поддерживать связывание данных. Разработчики ActiveX реализовали эту возможность так, чтобы она была достаточно гибкой. Элемент ActiveX можно написать так, чтобы любые из его свойств (одно или несколько) являлись связанными. Более того, контейнер, в который внедряется элемент, обладает полной свободой в выборе действий при изменении значения свойства или объекта, с которым связывается данное свойство. Соответственно, связывание может осуществляться не только по отношению к столбцам базы данных, но и к любому источнику данных, включая потоки данных, поступающие в реальном времени, другие элементы и т. д. Второй случай (другой элемент) очень часто встречается в программах на Visual Basic, поскольку элементы очень часто становятся основными «поставщиками» данных для программы (в этом случае связывание данных также может существенно усложняться, поскольку элемент-источник данных может предоставить интерфейс для просмотра нескольких записей). Связывание свойств в элементах ActiveX реализуется чрезвычайно просто. Связанное свойство оповещает контейнер об изменении своего значения, а контейнер может поступить с этим оповещением так, как считает нужным. Более того, свойство может запросить у контейнера разрешение на свое изменение еще до того, как это изменение произойдет.
www.books-shop.com
Вполне естественно, что процесс оповещения контейнера осуществляется с помощью того же механизма, как и обработка событий — то есть через точки соединения. Элемент, поддерживающий связывание свойств, должен реализовать точку соединения для интерфейса IPropertyNotifySink. Затем контейнер реализует этот интерфейс, подключает его к точке соединения элемента и в дальнейшем получает оповещения о всех изменениях свойств, объявленных как связываемые. Тот же самый интерфейс используется и для того, чтобы элемент мог запросить у контейнера, может ли он изменить значение того или иного свойства. Связываемые свойства объявляются в библиотеке типов. Стандартный набор атрибутов IDL/ODL был расширен для поддержки связывания данных. Если в ODL-файле свойство встречается несколько раз (например, в функциях для чтения и записи), соответствующие атрибуты должны быть указаны во всех случаях. Первый атрибут, Bindable, сообщает о том, что свойство будет оповещать контейнер о своем изменении. Атрибут RequestEdit указывается для свойств, которые перед изменением своего значения будут пытаться вызвать метод контейнера IPropertyNotifySink::OnRequestEdit. Поскольку свойства, поддерживающие запрос на изменение, также должны быть связываемыми, помимо RequestEdit должен указываться атрибут Bindable. За выполнением этого обязательного условия следит компилятор библиотеки типов (MIDL или MkTypeLib). Атрибут DisplayBind может указываться только для свойств с атрибутом Bindable. Он сообщает контейнеру о том, что при наличии у него механизма для пометки связываемых свойств элемента такая пометка должна быть осуществлена. Если связываемое свойство не желает, чтобы конечный пользователь знал о факте связывания, то ему не следует устанавливать этот флаг. Атрибут DefaultBind также может быть задан только для свойств с атрибутом Bindable; он сообщает контейнеру о том, что данное свойство наиболее полно представляет элемент и, следовательно, логичнее всего установить связь именно с ним. Классическим случаем такого «связанного по умолчанию свойства» является текст в текстовом поле. Лишь одно свойство в каждом элементе может обладать таким атрибутом.
Подробнее об интерфейсе IPropertyNotifySink Интерфейс IPropertyNotifySink содержит два метода:
HRESULT OnChanged(DISPID dispid); HRESULT OnREquestEdit(DISPID dispid); Метод OnChanged вызывается элементом при изменении значения связанного свойства. Параметр метода dispid определяет измененное свойство. Обратите внимание на то, что метод вызывается после изменения. OnRequestEdit вызывается элементом перед изменением значения свойства с флагом «запроса на изменение». Если контейнер возвращает S_OK, свойство может быть изменено; при возвращении S_FALSE значение свойства должно остаться прежним. Кроме того, во время вызова OnRequestEdit контейнер может выполнить и другие действия — например, получить и сохранить текущее значение свойства. Элемент, для свойств которого поддерживаются запросы на изменение, должен уметь реагировать на отказ контейнера, отменять действие, приводящее к изменению свойства, и обеспечивать вызов метода перед тем, как произойдет фактическое изменение свойства. Связанные свойства должны вызывать OnChanged независимо от способа изменения свойства — из программы, в результате интерактивных действий пользователя или любыми другими средствами, включая страницы свойств. Исключениями является только создание элемента, его чтение с диска или загрузка из памяти. В момент создания элемента свойства считаются уже
www.books-shop.com
изменившимися, так что никакие дополнительные оповещения не требуются. Если в результате одной операции изменяется сразу несколько свойств, элемент не обязан вызывать OnChanged для каждого отдельного свойства. Вместо этого он может вызвать OnChanged с dispid, равным DISPID_UNKNOWN (–1). Тем самым он сообщает контейнеру о том, что изменилось сразу несколько свойств и что контейнер может получить значения тех из них, которые его интересуют. Интерфейс IPropertyNotifySink также используется средами разработки (типа Visual Basic) и для других целей. Visual Basic может вывести немодальное окно, в котором перечисляются все свойства текущего выделенного элемента. Разумеется, все изменения в этом окне синхронизируются с текущим состоянием элемента. Тем не менее значения свойств элемента могут изменяться и другими способами — например, через страницы свойств (о них будет рассказано ниже в этой главе). Окно свойств Visual Basic не знает о том, что значение свойства было изменено через страницу свойств данного элемента. Тем не менее если каждое свойство, которое может быть изменено таким образом, будет помечено как связываемое, то «долг чести» элемента заставит его вызвать метод контейнера IPropertyNotifySink::OnChanged. В режиме конструирования Visual Basic перехватывает этот вызов и пользуется им для вывода оперативной информации в окне свойств.
3.17 Взаимодействие элемента с контейнером Спецификация Элементов ActiveX определяет не только механизм обработки событий, но и набор интерфейсов, облегчающих взаимодействие элемента с контейнером. Эти интерфейсы используются для передачи информации (например, об изменениях свойств окружения) и не имеют непосредственного отношения к программированию элементов. Один интерфейс, IOleControl, реализуется элементом, а другой, IOleControlSite, реализуется контейнером. IOleControl содержит четыре метода:
Метод GetControlInfo возвращает структуру CONTROLINFO, которая сообщает контейнеру о том, как должны обрабатываться нажатия клавиш на клавиатуре. Эта тема более подробно рассматривается в следующем разделе («Работа с клавиатурой») вместе с методом OnMnemonic, который вызывается контейнером при нажатии клавиш из таблицы акселераторов данного элемента. Метод OnAmbientPropertyChange вызывается при изменении контейнером одного или нескольких свойств окружения. Если было изменено одно свойство, то передаваемый методу параметр представляет собой dispid измененного свойства. Если изменяются два и более свойства, то параметр равен DISPID_UNKNOWN (–1), а элемент должен запросить у клиентского узла новые значения всех используемых им свойств окружения. При помощи метода FreezeEvents контейнер может запретить элементу возбуждать события. FreezeEvents содержит счетчик количества своих вызовов: вызовы с параметром TRUE увеличивают значение счетчика, а вызовы с параметром FALSE уменьшают его. При первоначальной загрузке элемента значение счетчика равно 0, поэтому элемент возбуждает события. При запрете на возбуждение событий элемент может заносить их в очередь, чтобы возбудить их после снятия запрета; возможен и другой вариант — попросту отбрасывать все возникающие события. Выбор решения зависит от элемента. IOleControlSite содержит семь методов:
www.books-shop.com
HRESULT HRESULT HRESULT HRESULT
OnControlInfoChanged (void); LockInPlaceActive(BOOL fLock); GetExtendedControl(IDispatch **ppDis); TransformCoords(POINTL *lpptlHimetric, POINTF *lpptfContainer, DWORD flags); HRESULT TranslateAccelerator(MSG *lpMsg, DWORD grfModifiers); HRESULT OnFocus(BOOL fGotFocus); HRESULT ShowPropertyFrame(void); Методы OnControlInfoChanged и TranslateAccelerator выполняют специализированные задачи при обработке нажатий клавиш и рассматриваются в разделе «Работа с клавиатурой». LockInPlaceActive вызывается элементом для того, чтобы сообщить контейнеру о временном запрете на вывод его из состояния активизации на месте. Обычно его использование связано с возбуждением событий, если выход из состояния активизации на месте может привести к каким-либо проблемам. Заблокированный элемент не может перейти в загруженное или рабочее состояние, поскольку это приведет к его деактивизации. GetExtendedControl возвращает указатель на интерфейс Automation, реализуемый клиентским узлом при агрегировании элемента в том случае, если контейнер реализует объект расширенного элемента. Тем самым элемент получает возможность определять текущие значения расширенных свойств (управляемых клиентским узлом). Метод TransformCoords рассматривается на стр. 139. Метод OnFocus используется элементами для того, чтобы сообщить контейнеру о получении им фокуса ввода. С его помощью можно выполнить некоторые действия, непосредственно предшествующие UI-активизации элемента, при которой элемент и так получает фокус ввода. Последний метод, ShowPropertyFrame, относится к страницам свойств, которые рассматриваются в разделе «Страницы свойств» оставшейся части этой главы.
3.18 Работа с клавиатурой Внедренные объекты могут обрабатывать нажатия клавиш по своему усмотрению. Тем не менее некоторые служебные сочетания клавиш (акселераторы) выполняют особые функции и не передаются внедренным объектам. Кроме того, поскольку элементы ActiveX проникают в самые разные области рынка программ-компонентов, иногда приходится иметь дело с элементами типа надписей (labels), вся работа которых сводится к передаче фокуса другому элементу. В стандартных диалоговых окнах Windows надписи обычно реализуются в виде строк статического текста, причем одна буква такой строки служит мнемоническим сокращением (мнемоникой). При нажатии соответствующей клавиши менеджер диалогового окна активизирует следующий элемент, способный получить фокус ввода. Элементы ActiveX должны обладать похожими функциями или хотя бы располагать механизмом для этого. Немедленно возникает закономерный вопрос: «Как это сделать?» Элемент сообщает контейнеру, какие акселераторы представляют для него интерес, передавая ему (среди прочего) логический номер своей таблицы акселераторов при вызове контейнером метода IOleControl::GetControlInfo. Контейнер запоминает эту информацию для каждого внедренного в него элемента. Если элемент динамически изменяет ее (конечно, он имеет на это полное право), он вызывает IOleControlSite::OnControlInfoChanged. Тем самым он сигнализирует контейнеру о том, что тот должен снова вызвать IOleControl::GetControlInfo для данного элемента. Информация, которая передается контейнеру в структуре, возвращаемой GetControlInfo, включает набор флагов, сообщающих, пользуется ли элемент клавишами Enter или Esc в UI-активном состоянии. Благодаря этим флагам контейнер может решить некоторые проблемы, связанные с UIактивностью — например, следует ли ему выделить кнопку, принятую по умолчанию (в этом случае нажатая пользователем клавиша Enter приведет к срабатыванию данной кнопки).
www.books-shop.com
Контейнер может решить, какие сочетания клавиш будут передаваться внедренным элементам в качестве акселераторов. Например, элемент может обрабатывать сочетание Ctrl+Enter, однако контейнер резервирует сочетания типа Ctrl+клавиша для своих целей, и тогда до элемента они не доходят. Если полученное сочетание клавиш будет опознано как мнемоника элемента, контейнер вызывает IOleControl::OnMnemonic. Элемент может сделать то, что считает нужным. Элементы, выполняющие функции кнопок, нуждаются в особом обращении со стороны контейнера. Сначала они сообщают контейнеру о своем желании «быть кнопкой», устанавливая бит состояния OLEMISC_ACTSLIKEBUTTON (см. раздел «Биты состояния» ниже в этой главе). Когда контейнер, поддерживающий эту концепцию, обнаруживает, что элемент будет выполнять функции кнопки, он должен соответствующим образом отреагировать на это. Кнопка может получить значение свойства окружения DisplayAsDefaultButton и по нему определить, должна ли она своим внешним видом сообщить пользователю о том, что она является кнопкой по умолчанию. При нажатии клавиши Enter такая кнопка будет активирована; однако это происходит только для элементов, которые сами не используют клавишу Enter. Контейнер присваивает этому свойству окружения соответствующее значение для каждого внедренного «кнопочного» элемента. Если клавиша Enter нажата при выделенном элементе-кнопке, то контейнер вызывает метод IOleControl::OnMnemonic этого элемента. Кнопку также можно назначить отменяющей (Cancel button), хотя сама кнопка об этом ничего не знает — эта концепция реализуется на уровне контейнера. В этом случае контейнер при нажатии клавиши Esc активизирует отменяющую кнопку. Наконец, существует проблема переключателей (radio buttons) или кнопок любого типа, которые действуют как переключатели. «Переключателем» называется кнопка, работающая вместе с несколькими аналогичными ей; в любой момент времени может быть активна лишь одна кнопка из такой группы, а щелчок на такой кнопке снимает активность со всех остальных элементов группы. Вероятно, вы уже догадываетесь о том, что все эти действия выполняются контейнером, однако кнопки-переключатели должны как-то сообщить о своих функциях и предоставить контейнеру возможность установки и снятия отдельных кнопок группы. Помимо установки флага OLEMISC_ACTSLIKEBUTTON, кнопка сообщает о выполнении функций переключателя и одновременно предоставляет контейнеру требуемый механизм, если она обладает свойством Value типа ExclusiveBool (которое также должно быть свойством по умолчанию). В соответствии со стандартной библиотекой типов Элементов OLE, этот тип может принадлежать лишь свойству Value. Кроме того, данное свойство должно быть помечено как связываемое. Некоторые элементы, в особенности флажки (check boxes), обладают тремя состояниями: включенным, выключенным и серым (то есть неопределенным). Для таких элементов спецификация Элементов OLE снова определяет стандартный тип OLE_TRISTATE, который представляет собой перечисляемый тип со значениями для каждого состояния. Флажки и другие элементы, которые хотят обладать тремя состояниями, также должны иметь свойство Value данного типа.
3.19 Типы и координаты После краткого упоминания о стандартных типах будет логично познакомиться со всеми стандартными типами, предусмотренными в спецификации Элементов OLE 94. Стандартные типы могут пригодиться для представления информации в виде, не зависящем от контейнера, а также при сообщении некоторых конкретных сведений — например, о том, что кнопка
www.books-shop.com
работает как переключатель. OLE_COLOR содержит информацию о цвете, которая может использоваться элементами и их контейнерами. Например, стандартное свойство окружения BackColor имеет тип OLE_COLOR. Кроме того, имеется вспомогательная функция API OleTranslateColor, преобразующая значение OLE_COLOR в COLORREF (COLORREF — стандартный тип для представления цветов в Win32). Спецификация определяет еще несколько стандартных типов аналогичного назначения. Наибольший интерес представляют два типа, благодаря которым существенно упрощается разработка элементов: один предназначен для работы со шрифтами, а другой — c графическими объектами.
3.20 Стандартный шрифтовой объект Стандартный шрифтовой объект предоставляет единый механизм для создания и использования шрифтов в элементах ActiveX. Объект поддерживает интерфейс диспетчеризации для чтения и записи его свойств, а также интерфейс IFont для создания шрифтовых объектов и работы с ними. Методы интерфейса IFont перечислены в следующей таблице (существуют и другие методы, но они рассматриваются в контексте рассматриваемого ниже интерфейса IFontDisp).
Метод IFont
Описание
IsEqual
Определяет, совпадают ли два шрифтовых объекта Элементов ActiveX, то есть представляют ли они шрифты с одинаковыми характеристиками (это не означает, что они совместно используют один и тот же шрифтовой объект Windows).
Clone SetRatio
Создает новый шрифтовой объект Элементов ActiveX, характеристики которого совпадают с характеристиками существующего объекта. Задает коэффициент пропорциональности шрифта.
AddRefHfont
Увеличивает счетчик применений логического номера шрифта Windows (HFONT), представляемого данным объектом (подробнее см. ниже).
ReleaseHfont
Уменьшает счетчик применений HFONT, представляемого данным объектом (подробнее см. ниже).
Заполняет стандартную структуру Windows TEXTMETRIC QueryTextMetrics характеристиками шрифта для текущего контекста устройства. SetHDC
Предоставляет шрифтовому объекту доступ к контексту устройства (DC), в котором он может использоваться, чтобы объект мог определить характеристики DC (например, текстовые метрики).
Свойства шрифтового объекта, работа с которыми осуществляется средствами Automation, перечислены в следующей таблице. Большая часть этих свойств доступна как для чтения, так и для записи (за исключением hFont). Реализующий их интерфейс, IFontDisp, в настоящее время определяется как производный от IDispatch и не содержит никаких дополнительных методов. Все функции для работы со свойствами и методами на самом деле реализуются как методы интерфейса IFont, так что общение со шрифтовым объектом через интерфейс IFont оказывается значительно более эффективным, чем через IFontDisp. IFontDisp создан лишь для удобства тех пользователей, которым приходится управлять шрифтовыми объектами через IDispatch. Единственная проблема с IFont заключается в том, что некоторые из его методов имеют параметры, типы которых не поддерживаются в Automation.
www.books-shop.com
Проблема с посторонней реализацией шрифтовых объектов заключается в том, что довольно часто они воспроизводят базовый шрифтовой объект Windows при каждом изменении его свойств. Если вы изменяете и размер шрифта, и его насыщенность, то процесс будет длиться значительно дольше необходимого, потому что для каждого изменения будет создаваться новый шрифтовой объект Windows. Шрифтовой объект Элементов ActiveX ведет себя более разумно. Он не создает шрифтовой объект Windows до тех пор, пока это действительно не станет необходимым, поэтому одновременное изменение нескольких свойств не приводит к напрасным затратам времени. Кроме того, объект кэширует логические номера Windows HFONT, так что два идентичных шрифтовых объекта в одном процессе могут получить один и тот же базовый HFONT. Такой вариант также оптимизирует работу со шрифтами, однако он обладает побочным эффектом — возвращаемые объектом значения HFONT оказываются недолговечными. Любое изменение в этом или даже другом шрифте может сделать логический номер недействительным. Соответственно, приходится пользоваться методами AddRefHfont и ReleaseHfont интерфейса IFont для обновления счетчика применений HFONT, чтобы логический номер оставался действительным до тех пор, пока он вам нужен. Разумеется, при этом приходится отказываться от оптимизации, обеспечиваемой при кэшировании шрифтов, так что пользоваться этими методами следует разумно. Шрифтовой объект поддерживает интерфейсы IPersistStream, IPersistPropertyBag и IDataObject, поэтому он может сохранить себя на диске как в двоичном (IPersistStream), так и в текстовом формате (через IPersistPropertyBag или IDataObject). Обычно сохраняемый элемент предлагает каждому содержащемуся в нем шрифтовому объекту сохранить себя. OLE предоставляет функцию API OleCreateFontIndirect, предназначенную для создания шрифтовых объектов. Она получает указатель на новую структуру FONTDESC и возвращает указатель на интерфейс IFont созданного объекта. Элементы ActiveX должны пользоваться шрифтовыми объектами для задания всех свойств, связанных с выбором шрифта. Наконец, состояние шрифтового объекта может быть изменено пользовательской программой. Как сообщить об этих изменениях элементу, которому он принадлежит? И снова наглядно демонстрируется гибкость концепции точек соединения. Шрифтовой объект предоставляет точку соединения для IPropertyNotifySink, а элемент предоставляет реализацию этого интерфейса. Затем элемент подключается к точке соединения шрифтового объекта и вызывается при каждом изменении атрибутов шрифта.
Свойство шрифтового объекта Name
Описание Начертание шрифта — например, Times New Roman. Данное свойство имеет тип BSTR.
Size
Размер шрифта в пунктах. Данное свойство имеет тип CURRENCY — формат с фиксированной точкой, используемый в основном для денежных величин. Здесь он применяется для удобства, поскольку работает намного быстрее формата с плавающей точкой.
Bold
Определяет, является ли шрифт полужирным. Данное свойство, имеющее логический тип, связано со свойством Weight и подчиняется обычным шрифтовым конвенциям Windows. Если насыщенность шрифта превышает 550, он считается полужирным. Если присвоить этому свойству TRUE, насыщенности присваивается значение 700, а если присвоить FALSE — 400. См. ниже описание свойства Weight.
Italic
Определяет, является ли шрифт курсивным.
www.books-shop.com
Underline Strikethrough
Определяет, является ли шрифт подчеркнутым. Определяет, является ли шрифт зачеркнутым.
Weight
Допустимые значения насыщенности шрифта лежат в интервале от 0 до 1000, хотя чаще всего встречаются значения 400 (обычный) и 700 (полужирный). См. выше описание свойства Bold.
Charset
Задает кодировку — ANSI, Unicode или OEM.
hFont
Логический номер базового шрифтового объекта Windows.
3.21 Стандартный графический объект Стандартный графический объект по своей концепции очень близок к шрифтовому объекту. Он предоставляет стандартную возможность для представления и отображения «рисунков» — растров (bitmap), метафайлов или значков (icon), в соответствии с определением графического элемента в Visual Basic. Графические объекты, как и шрифтовые, обладают интерфейсом диспетчеризации IPictureDisp и интерфейсом IPicture; эти два интерфейса связаны между собой так же, как и IFontDisp и IFont. Методы IPicture (за исключениемтех, которые также реализуются как методы и свойства Automation) перечислены в следующей таблице.
Методы IPicture Render
Описание Заставляет графический объект воспроизвести себя в заданном контексте устройства.
Должен вызываться элементом, если он получил PictureChanged графический объект, логический номер базового объекта Windows и как-либо изменил изображение. SaveAsFile
Предлагает графическому объекту сохранить себя в виде файла.
get_CurDC
Этот метод, вместе с описанным ниже SelectPicture, помогает обойти ограничение Windows, согласно которому объект в любой момент времени может быть выбран только в одном контексте устройства. Поскольку графический объект может воспроизводиться многократно, он содержит методы для сохранения контекстов устройства между перерисовками.
SelectPicture
Выбирает рисунок в заданном контексте устройства и возвращает контекст устройства, в котором он ранее был выбран, вместе с логическим номером объекта GDI для рисунка. См. выше метод Get_CurDC.
В следующей таблице перечислены свойства, раскрываемые через интерфейс диспетчеризации IPictureDisp.
Свойство Handle
Описание Логический номер Windows для базового объекта GDI.
hPal
Логический номер палитры, в которой должен воспроизводиться данный рисунок.
Type
Флаг, который обозначает тип графического объекта — растр, метафайл или значок.
Width
Ширина объекта в единицах типа OLE_XSIZE_HI-METRIC (см. ниже раздел «Координаты»).
Height
Высота объекта в единицах типа OLE_YSIZE_HI-METRIC (см. ниже раздел «Координаты»).
www.books-shop.com
Читает или записывает текущее значение внутреннего (для данного объекта) флага, который определяет, должен ли объект всегда сохранять формат, в котором он был создан. В некоторых ситуациях объект может KeepOriginalFormat решить, что для повышения эффективности ему следует перейти к другому типу и отбросить исходный формат. При установке внутреннего флага исходный формат будет сохранен. По аналогии со шрифтовыми объектами, графические объекты оповещают элемент-владелец об изменении своих свойств через точку соединения для IPropertyNotifySink. Для создания графических объектов используются две функции OLE API. OleCreatePictureIndirect создает графический объект на основании существующего растра, метафайла или значка или же создает объект заново. OleLoadPicture создает графический объект из файла стандартного формата для растра, метафайла или значка.
3.22 Координаты Как сказано в приведенной выше таблице, свойства Width и Height стандартного графического объекта имеют типы OLE_XSIZE_HIMETRIC и OLE_YSIZE_HIMETRIC. Вероятно, это утверждение выглядит не вполне понятным без предварительных пояснений. Спецификация Элементов ActiveX определяет специальные типы для размеров и координат, в первую очередь потому, что разные контейнеры по-разному отображают рисунки и пользуются координатами. Более того, координатная модель определяется контейнером, а не элементом. Все это приводит к последствиям, выходящим за рамки обычных определений типов. Контейнер должен знать, какие параметры событий элементов представляют собой координаты, чтобы он мог преобразовать их в собственное координатное пространство. Например, если элемент инициирует событие Mouse Down (нажатие кнопки мыши), то элемент укажет координаты этого события, допустим, в пикселях. С другой стороны, контейнер может работать с координатами в дюймах, поэтому ему придется преобразовать пиксели в дюймы. Контейнер может определить, какие из параметров события нуждаются в таком преобразовании, просматривая библиотеку типов, — при условии, что каждому такому параметру был присвоен один из стандартных типов, определяемых OLE. Тем не менее для свойств и методов элемента такая схема обычно не работает. Дело в том, что контейнер не вмешивается в работу пользовательской программы или интерфейса, которые осуществляют доступ к этим свойствам. Иначе говоря, если у элемента есть свойство со значением 1234, то пользовательская программа желает получить именно 1234, а не какое-то по волшебству преобразованное значение, смысл которого понятен разве что самому контейнеру. Ситуация осложняется тем, что элементы ActiveX обладают механизмом для просмотра и изменения свойств, который на первый взгляд абсолютно не зависит от контейнера — речь идет о страницах свойств. Как же элемент может вывести значения координатных свойств в единицах, задаваемых контейнером? Элемент отвечает за значения свойств, но это не мешает ему через клиентский узел обратиться к контейнеру для осуществления преобразования. Интерфейс IOleControlSite содержит метод TransformCoords, вызываемый элементами для выполнения преобразования. Кроме того, элемент может сообщить контейнеру о том, какие из его свойств задаются в единицах контейнера — для этого ему следует воспользоваться другим набором определенных в OLE стандартных типов для координат. Контейнер также раскрывает название используемых единиц через строковое свойство окружения, ScaleUnits.
www.books-shop.com
3.23 Устойчивость Объект, который должен сохранить свое устойчивое состояние по внешнему запросу от контейнера, обычно реализует интерфейс IPersistStorage. Контейнер вызывает методы этого интерфейса через указатель на реализованный им интерфейс IStorage. Поскольку сохранение элемента обычно сводится к сохранению значений его свойств, использование IPersistStorage, пожалуй, является перебором. По этой причине спецификация Элементов ActiveX позволяет элементу ActiveX сохранить свое состояние в IStream. Хотя это вроде бы говорит о том, что предоставленного и реализованного OLE интерфейса IPersistStream будет достаточно для нужд элемента, на самом деле это не так — IPersistStream не имеет аналога метода InitNew интерфейса IPersistStorage. Во время загрузки элементу может потребоваться прочитать значения некоторых свойств окружения перед тем, как будет загружено сохраненное состояние. Без метода InitNew это оказывается невозможным, а значит, приводит к следующим последствиям:
Элемент не может прочесть значения свойств окружения до завершения загрузки. После завершения загрузки элемент может получить значения свойств окружения, которые отменяют некоторые загруженные свойства — это означает лишние затраты времени на загрузку неиспользуемых значений.
Усовершенствованный интерфейс устойчивости, IPersistStreamInit, справляется с этими проблемами. Он позволяет создать элемент таким образом, чтобы он мог читать и использовать значения свойств окружения еще до загрузки сохраненного состояния. Затем он может игнорировать некоторые фрагменты сохраненного состояния или даже отказаться от их сохранения в следующий раз. Чтобы это сработало, контейнер должен передать элементу объект его клиентского узла перед тем, как требовать от него загрузить сохраненное состояние. Элемент сообщает контейнеру о поддержке данной возможности, устанавливая бит состояния OLEMISC_SETCLIENTSITEFIRST (эта тема подробнее рассмотрена ниже в разделе «Биты состояния»). Контейнеры, написанные до появления спецификации Элементов ActiveX, не знают о существовании этого бита и IPersistStreamInit. Следовательно, элемент, желающий сохранить совместимость со старыми контейнерами, должен поддерживать IPersistStorage. Контейнеру следует определить присутствие бита OLEMISC_SETCLIENTSITEFIRST и позаботиться о том, чтобы перед загрузкой сохраненного состояния элемента был вызван метод SetClientSite. Разумеется, это могут сделать лишь новые контейнеры, знающие о существовании элементов, поэтому элемент сам может определить, поддерживает ли контейнер эту концепцию. Для этого ему следует проверить, вызывается ли метод SetClientSite перед вызовом метода InitNew или Load. Единственное отличие IPersistStream от IPersistStreamInit заключается в том, что в последний интерфейс добавлен метод InitNew. Тем не менее IPersistStreamInit не может быть сделан производным от IPersistStream, поскольку это привело бы к использованию контейнером стандартного интерфейса IPersistStream. В итоге метод InitNew вообще бы не вызывался, а новый интерфейс утратил бы всякий смысл.
3.24 Наборы и комплекты свойств Пользователи Microsoft Visual Basic знают, что программу на Visual Basic можно сохранить в текстовом формате, чтобы она (большей частью) могла быть прочитана человеком. В текст входят значения устойчивых свойств всех элементов, используемых в программе. В тех случаях, когда данные не удается легко и осмысленно преобразовать в текст, Visual Basic сохраняет
информацию в двоичных файлах (например, формата FRX). Каким образом контейнер наподобие Visual Basic сможет поддерживать эту возможность в новом мире элементов ActiveX?
3.25 Сохранение в текстовом формате — старый способ Способ оповещения об изменении свойств (при связывании данных) наводит на мысль, что элементы ActiveX стараются переложить как можно больше черной работы на контейнер. В нашем случае наблюдается та же картина! Элемент ActiveX предоставляет контейнеру ровно столько информации, сколько необходимо для сохранения его устойчивых свойств в текстовом виде. Передаваемая информация содержится в структуре данных OLE, которая называется «набором свойств» (property set). Спецификация набора свойств, приведенная в конце справочника OLE 2 Programmer’s Reference, Volume 1, выглядит довольно туманно, поэтому разработчики спецификации Элементов ActiveX любезно довели ее до ума и сделали более содержательной. Я не стану подробно разбирать спецификацию наборов свойств, а ограничусь тем, что набор свойств представляет собой таблицу с идентификаторами свойств (не путать с dispid!) и значениями. Идентификаторы, используемые в наборах свойств Элементов ActiveX, соответствуют именам свойств, а тип значений определяется в зависимости от типов свойств. Помимо значений стандартных типов, которые могут быть присвоены типу данных Automation VARIANT, наборы свойств также позволяют сохранять BLOB различного рода (сокращение «BLOB» означает «большой двоичный объект», то есть любой блок двоичных данных произвольного содержания). Задача контейнера — прочитать набор свойств и по возможности преобразовать его в текстовый формат. Все, что не может быть сохранено в текстовом формате, должно остаться в виде двоичных данных, однако сама идея сохранения в текстовом формате заключается в том, чтобы как можно большую часть состояния объекта представить в понятной для человека форме. Текстовое представление, сохраненное контейнером, должно быть семантически эквивалентно тому, которое сохраняется методом Save в IPersistStream или IPersistStorage, а значит, оно должно содержать ту же информацию. Аналогично, набор свойств, передаваемый от контейнера к элементу при чтении текстового формата, должен быть семантически эквивалентным методу Load интерфейса IPersistStream или IPersistStorage. Это означает, что набор свойств должен давать полную информацию для восстановления сохраненного состояния элемента. Элемент получает и передает наборы свойств через реализованный им интерфейс IDataObject.
3.25.1 Сохранение в текстовом формате — новый способ В Visual Basic описанный выше способ для сохранения и чтения свойств в текстовом формате был заменен новым, более эффективным, и сейчас этот способ считается предпочтительным. Однако многие элементы на программном рынке продолжают пользоваться механизмом, описанным в предыдущем разделе, поэтому я и остановился на нем. Работа нового механизма построена на двух интерфейсах — IPropertyBag, который действует со стороны контейнера, и IPersistPropertyBag, действующем со стороны элемента. Механизм также пользуется третьим интерфейсом, IErrorLog. В общих чертах схема выглядит так: контейнер предоставляет «комплект свойств» (property bag) через свою реализацию интерфейса IPropertyBag. «Комплект» представляет собой набор значений
www.books-shop.com
свойств данного элемента. Через свою реализацию IPersistPropertyBag элемент может потребовать у комплекта свойств принять или отвергнуть значение некоторого именованного свойства в виде VARIANT. Любые ошибки, возникающие при передаче свойств (например, если в комплекте нет свойства с таким именем), могут передаваться в виде структуры EXCEPINFO реализации IErrorLog. Затем эта реализация может записать текстовый файл с информацией о том, какие свойства не были успешно прочитаны. Теперь давайте соберем все сказанное воедино в контексте «сохранения в текстовом формате». Сначала элемент реализует интерфейс IPersistPropertyBag, производный от IPersist и обладающий следующими дополнительными методами:
HRESULT InitNew(); HRESULT Load(IPropertyBag *pPropBag, IErrorLog *pErrorLog); HRESULT Save(IPropertyBag *pPropBag, BOOL fClearDirty, BOOL fSaveAllProperties); Метод InitNew аналогичен IPersistStreamInit::InitNew; он сообщает объекту о том, что тот инициализируется при создании нового объекта и не обладает значениями устойчивых свойств, которые бы следовало прочитать. Load требует от объекта загрузить все его свойства из передаваемого комплекта и записать все возникшие ошибки в протокол ошибок, передаваемый в качестве второго параметра. Save требует от объекта сохранить значения его свойств — если параметр fSaveAllProperties равен TRUE, то в комплекте должны быть сохранены все устойчивые свойства, если он равен FALSE, то объект должен сохранить лишь те значения, которые изменились с момента последнего сохранения. При этом элемент не обязан различать эти два состояния, параметр всего лишь предоставляет возможность для оптимизации. Наконец, флаг fClearDirty сообщает объекту, может ли он считать себя «чистым» после завершения сохранения. Контейнер реализует интерфейс IPropertyBag, производный от IUnknown и содержащий следующие дополнительные методы:
HRESULT Read(LPCOLESTR pszPropName, VARIANT *pVar, IErrorLog *pErrorLog); HRESULT Write(LPCOLESTR pszPropName, VARIANT *pVar); Также он реализует интерфейс IErrorLog, производный от IUnknown и содержащий один дополнительный метод:
HRESULT AddError(LPCOLESTR pszPropName, LPEXCEPINFO pExcepInfo); Когда контейнеру понадобится, чтобы элемент сохранил себя (обычно таким образом, чтобы контейнер затем мог сохранить устойчивое состояние элемента в текстовом формате), он вызывает метод элемента IPersistPropertyBag::Save, передавая ему указатель на реализацию IPropertyBag. Затем элемент при помощи итератора перебирает все свои устойчивые свойства и решает, какие из них следует сохранить. Каждое такое свойство сохраняется при помощи метода IPropertyBag::Write, которому передается имя свойства и его значение в том виде, который будет выбран элементом. Аналогично, при повторном создании элемента или в ином случае, когда элемент должен загрузить свои свойства, контейнер вызывает метод IPersistPropertyBag::Load и передает ему указатели на интерфейсы IPropertyBag и IErrorLog. Элемент также перебирает все свои устойчивые
www.books-shop.com
свойств значения по умолчанию или же объявить неудачной всю операцию, если данные свойства являются критически важными и присвоение значений по умолчанию не имеет смысла. Метод IErrorLog::AddError чрезвычайно прост. Ему передается имя свойства и стандартная структура Automation EXCEPINFO. Контейнер может сделать с ними все, что считает нужным. В общем случае он может записать информацию в файл или привлечь к ней внимание средствами пользовательского интерфейса. Для элементов ActiveX предусмотрены и другие средства обеспечения устойчивости, но они скорее относятся к теме Internet и не будут рассматриваться до главы 13, «Элементы ActiveX и Internet».
3.26 Биты состояния Перед тем как загружать сервер и создавать экземпляры его объектов, контейнер может захотеть получить о нем некоторые сведения. COM позволяет сделать это через категорию реестра CLSID для класса MiscStatus. Если данный элемент реестра существует (что, вообще говоря, необязательно), то он содержит набор двоичных флагов (битов), конкретный смысл которых определяется COM. Объект также может иметь различные наборы битов для различных аспектов (то есть представлений), они хранятся в виде отдельных ключей в категории MiscStatus. Если для того или иного аспекта существует ключ нижнего уровня, он получает имя в соответствии с числовым представлением данного аспекта (например, биты MiscStatus для аспекта DVASPECT_ICON хранятся с ключом 4). Значения, относящиеся к конкретному аспекту, отменяют действие значений по умолчанию, которые хранятся с ключом MiscStatus. Контейнер получает значения битов состояния, вызывая метод IOleObject:: GetMiscStatus или непосредственно читая содержимое реестра. Реализация этого метода, принадлежащая стандартному обработчику, не загружает объект в том случае, если он не работает в данный момент. Значения битов определяются спецификацией COM, а спецификация Элементов ActiveX дополняет этот список. В исходном списке COM к элементам относятся следующие биты:
OLEMISC_INSIDEOUT — сообщает контейнеру о том, что данный объект хочет обладать внутренней активизацией (то есть активизироваться на месте и при этом быть UI-активным). OLEMISC_ACTIVATEWHENVISIBLE — сообщает контейнеру, что объект должен активизироваться в тот момент, когда он становится видимым, даже если он не является UI-активным.
В следующей таблице перечислены некоторые флаги, добавленные спецификацией Элементов ActiveX.
Флаг
Описание
Бит должен устанавливаться теми элементами, которые не должны отображаться во время выполнения программы (например, элемент-таймер). OLEMISC_INVISIBLEATRUNTIME Если контейнер поддерживает концепцию «режима выполнения», он должен учитывать значение этого бита и скрывать элемент в этом режиме. OLEMISC_ALWAYSRUN
Бит сообщает контейнеру о том, что объект хочет всегда находиться в рабочем состоянии и что стандартный обработчик не должен откладывать загрузку объекта на
www.books-shop.com
последний момент. Поскольку большинство элементов ActiveX реализуется в виде внутрипроцессных серверов, обычно нет необходимости в указании данного бита.
OLEMISC_ACTSLIKEBUTTON
Бит сообщает контейнеру о том, что данный элемент выполняет функции кнопки. В частности, это позволяет контейнеру сообщить элементу, чтобы он отобразил себя в виде кнопки по умолчанию или же стандартной кнопки.
OLEMISC_ACTSLIKELABEL
Стандартные диалоговые окна Windows пользуются надписями (объектами оконного класса static) для того, чтобы пометить элемент и определить его мнемонику. Если реализуемый элемент ActiveX должен обладать функциями надписи, необходимо установить этот бит. Он позволяет контейнеру правильно обрабатывать данный элемент при передаче фокуса тому элементу, помеченному с его помощью.
OLEMISC_NOUIACTIVATE
Бит сообщает контейнеру о том, что элемент не обладает пользовательским интерфейсом, который можно было бы активизировать, и работать с ним можно только средствами Automation и при помощи событий. Обратите внимание на то, что элемент может сообщить об отсутствии у него отдельного состояния активизации на месте, не устанавливая бит OLEMISC_ INSIDEOUT.
OLEMISC_ALIGNABLE
Бит используется контейнерами, поддерживающими выравнивание элементов. Он устанавливается элементом, который было бы желательно выровнять по одной из сторон контейнера. Контейнеры, поддерживающие выравнивание элементов, могут на основании этого бита решить, следует ли разрешить пользователю выровнять тот или иной элемент.
OLEMISC_IMEMODE
В международных версиях Windows, использующих многобайтовую кодировку MCBS (например, в японской версии Windows), можно создавать расширенные символы при помощи редакторов ввода (input method editors, IME). Бит сообщает о том, что элемент поддерживает работу таких редакторов. Обычно контейнер, также поддерживающий IME, предоставляет в распоряжение элемента расширенное свойство IMEMode.
OLEMISC_SIMPLEFRAME
Представьте, что написанный вами элемент ActiveX предназначен всего лишь для хранения других элементов (примером может послужить групповой элемент, внутри которого обычно размещаются переключатели, флажки и т. д.). Поскольку вся область внутри элемента принадлежит ему самому, все размещаемые в ней элементы должны обрабатываться особым образом. Пометка элемента этим битом говорит о том, что он поддерживает интерфейс ISimple Frame Site и также выполняет некоторые функции контейнера,
www.books-shop.com
хотя вся основная работа поручается настоящему контейнеру. Для обычных внедренных объектов OLE клиентский узел создается при загрузке контейнером с диска их устойчивого состояния (метод OleLoad) или при инициализации нового экземпляра методом IPersistStorage::InitNew. Тем не менее у элементов ActiveX может возникнуть OLEMISC_SETCLIENTSITEFIRST необходимость в обращении к клиентскому узлу на очень ранней стадии создания (например, для получения текущих значений свойств окружения) и до загрузки их устойчивого состояния. При помощи данного флага элемент сообщает о такой необходимости. Контейнеры OLE, не знающие о существовании элементов ActiveX или новых битов MiscStatus, будут игнорировать значения этих битов. Следовательно, элементы должны приготовиться к тому, чтобы существовать при отсутствии полноценной поддержки со стороны контейнера. Существующие объекты OLE, не являющиеся элементами, также не пользуются этими битами, однако новые контейнеры, предназначенные для работы с элементами, должны учитывать значения битов состояния и обходиться с ними должным образом.
3.27 Страницы свойств Во всех моделях нестандартных элементов до появления спецификации Элементов ActiveX существовали средства, при помощи которых контейнер мог получить значения свойств элемента и отобразить их для просмотра и изменения. Например, в Visual Basic для этой цели использовалось окно свойств. Тем не менее для некоторых контейнеров подобный интерфейс неприемлем — примером может послужить оболочка операционной системы, наделенная возможностью внедрения элементов ActiveX. Следует учитывать и тот факт, что существующие парадигмы пользовательских интерфейсов становятся все более и более «объектно-центрическими» (то есть сначала выбирается некоторый объект, затем с ним выполняются нужные действия). Возникает необходимость в методике отображения и изменения свойств элемента, которая бы не зависела от контейнера. С этой целью в спецификации Элементов ActiveX были предусмотрены страницы свойств. «Страницей свойств» называется специальный интерфейс, реализованный элементом, при помощи которого можно просматривать и задавать значения свойств элемента (см. также главу 11). Поскольку количество свойств у элемента может быть достаточно большим, любой элемент может иметь произвольное количество страниц свойств. Кроме того, существуют стандартные системные страницы свойств для шрифтовых и графических объектов, а также для цветов. Общий принцип выглядит так: некий объект реализует «фрейм свойств» (property frame), в котором страницы свойств могут отображаться в соответствии с общепринятыми стандартами пользовательского интерфейса. В настоящее время таким стандартом являются диалоговые окна со вкладками. Классическое диалоговое окно с вкладками изображено на рис. 3-4.
www.books-shop.com
Рис. 3-4.Страницы свойств, отображаемые в диалоговом окне с вкладками — текущем стандарте пользовательского интерфейса Для элемента страницы свойств представляют собой шаблоны диалоговых окон. В этих шаблонах предусмотрены поля для всех свойств, раскрываемых элементом через страницы, а код для работы с диалоговыми окнами управляет их содержимым. Страницы свойств представляют собой COM-объекты с собственными CLSID и интерфейсами. Это позволяет нескольким элементам пользоваться одними и теми же страницами свойств, если они обладают одинаковыми свойствами. Существуют стандартные реализации страниц свойств, которыми можно пользоваться в общих случаях. Страница свойств раскрывает COMинтерфейс IPropertyPage, содержащий методы для создания узла страницы свойств внутри фрейма, активизации и деактивизации страницы и т. д. Для каждой страницы свойств, предоставленной элементом, фрейм реализует узел через интерфейс IPropertyPageSite. Фрейм может определить совокупность страниц свойств, раскрываемых элементом, при помощи интерфейса IspecifyPropertyPages, содержащего всего один метод GetPages. Метод возвращает массив CLSID для страниц свойств, что позволяет создать каждую из них при помощи, например, CoCreateInstance. Runtime-система Элементов ActiveX предоставляет стандартную реализацию для фрейма, к которой можно обратиться при помощи функции OleCreatePropertyFrame. Microsoft Windows 95 и другие версии Windows также содержат стандартные реализации страниц свойств. Фреймы свойств могут обслуживать сразу несколько объектов, а это означает, что от страницы свойств можно потребовать отобразить свойства для нескольких элементов. Значения, которые отображаются средствами контейнера для просмотра свойств (например, в окне свойств Visual Basic), и теми, что отображаются страницами, могут не совпадать. Средства просмотра свойств обычно также отображают расширенные свойства элемента — например, его положение и размер, тогда как для страницы свойств это будет нелогично. Кроме того, различные средства просмотра нередко предоставляют различные механизмы пользовательского интерфейса для изменения значений свойств определенных типов. Например, для изменения свойств BackColor и ForeColor, отображаемых Visual Basic в окне свойств, можно выбрать нужный цвет в диалоговом окне или же непосредственно ввести шестнадцатеричное значение (хотя это и не очень удобно). Вероятно, со временем страницы свойств обзаведутся собственными механизмами, которые позволят объединить изменения разнообразных свойств в едином пользовательском интерфейсе. Постепенно будут выработаны стандарты, которых будет
www.books-shop.com
придерживаться весь мир — это облегчит жизнь как пользователям, так и нам, программистам.
3.28 Работа с отдельными свойствами Я должен обязательно упомянуть еще об одной вещи — концепции «работы с отдельными свойствами». При помощи механизма страниц можно одновременно просматривать и редактировать целые группы свойств, однако некоторые контейнеры должны обладать возможностью работы с отдельными свойствами. В большинстве случаев это можно сделать через библиотеку типов данного элемента, однако в этом случае не удастся проверить значения, присвоенные элементам. К тому же этот способ не годится для тех типов, которые не могут непосредственно использоваться средствами Automation. Если объект обладает свойством (или несколькими свойствами), с которыми контейнер работает отдельно, он должен реализовать интерфейс IPerPropertyBrowsing. Методы этого интерфейса возвращают имя свойства в пригодном для отображения виде, набор заранее определенных строковых или числовых значений данного свойства, а также позволяют работать с ним через страницу свойств элемента. Чтобы упростить последнюю возможность, страницы свойств также могут реализовать интерфейс IPropertyPage2, который отличается от IPropertyPage лишь наличием метода EditProperty, устанавливающего фокус страницы на конкретном поле свойств. Если элемент не поддерживает этот интерфейс, фокус принадлежит первому элементу на странице. Разумный подход к написанию элемента значительно упрощает реализацию страниц свойств, поэтому вряд ли можно оправдать отсутствие поддержки страниц там, где она нужна. Для контейнеров, не умеющих работать со страницами свойств, была добавлена новая команда (verb) OLE с именем Properties, который будет отображаться контейнером при выделении элемента со страницами свойств. Выполнение этой команды приводит к появлению стандартного фрейма, заполненного страницами свойств выделенного элемента.
3.29 Лицензирование Позднее я посвящу теме лицензирования целую главу (см. главу 16), а сейчас мы рассмотрим лишь расширения, внесенные в COM спецификацией Элементов ActiveX. Основная проблема программ-компонентов связана с тем, что они могут использоваться в самых разных местах — именно для этого они и разрабатываются. Для пользователей это просто замечательно… однако у продавцов программного обеспечения возникает множество хлопот! Если в распространяемом приложении присутствует ваш элемент, то, вероятно, вам заплатили за право его использования. Однако пользователи могут взять этот элемент и бесплатно использовать его в своих собственных приложениях — и вы понесете убытки. К счастью, на самом деле все не так трагично. Существует механизм, который разрешает использовать элемент ActiveX в одних обстоятельствах и запрещает это делать в других, в зависимости от состояния лицензии. Следует хорошо представлять различные ситуации использования элемента:
Использование в режиме конструирования. Использование в режиме выполнения для конкретного приложения. Общее использование в режиме выполнения.
Отличия сводятся к возможности создавать новые приложения (режим конструирования) по сравнению с возможностью использовать элемент в готовом приложении (режим выполнения). Ситуация несколько усложняется тем, что для некоторых контейнеров режимы конструирования и выполнения
www.books-shop.com
не различаются, но пока мы не будем вдаваться в подробности. Два случая для режима выполнения позволяют лицензировать использование элемента в одном приложении или в любом их количестве. Модель лицензирования Элементов ActiveX спроектирована так, что при своей относительной простоте она удовлетворяет всем требованиям, перечисленным выше. Кроме того, ее можно легко расширить для поддержки более сложных схем или механизмов лицензирования. Например, для элемента можно довольно просто организовать поддержку нескольких уровней лицензирования, чтобы его пользователи могли получать доступ к различным наборам функций в зависимости от имеющейся у них лицензии. Работа с лицензионными расширениями COM осуществляется через интерфейс IClassFactory2. Он представляет собой разновидность IClassFactory (и на самом деле является производным от него) и содержит три дополнительных метода, которые позволяют контейнеру получить от элемента лицензионную информацию (GetLicInfo), потребовать у элемента ключ для использования в режиме выполнения (RequestLicKey) или создать экземпляр элемента на основе лицензионного ключа, полученного от контейнера (CreateInstanceLic). Метод GetLicInfo возвращает лицензионную информацию в виде структуры LICINFO, содержащей три поля:
Размер структуры. Флаг, определяющий, поддерживает ли объект ключ времени выполнения. Флаг, определяющий, проверил ли объект компьютер или лицензию пользователя.
Изучение лицензирования в этой главе скорее ограничивается обсуждением возможностей системы, а не их реализации. Первым делом следует учесть, что контейнеры, пользующиеся интерфейсом IClassFactory2, знают о лицензировании и, следовательно, могут пользоваться всеми его возможностями. Программы, не знающие о существовании интерфейса, продолжают пользоваться IClassFactory. Любое приложение, которое пользуется функциями-оболочками OLE (такими, как OleCreateInstance или OleCreate), явным образом обращается к IClassFactory, и его уже не удастся заставить работать с IClassFactory2. Следовательно, поддержка лицензионной схемы элементов в таких приложениях ограничена. Не забывайте, что интерфейс IClassFactory2 содержит метод RequestLicKey, при помощи которого контейнер элемента может запросить ключ, используемый в режиме выполнения. Что это значит? Давайте представим ситуацию, в которой вы пишете приложение, пользуясь «посторонним» элементом ActiveX. Если такой элемент захочет ограничить свое применение только этим приложением, пока вы (или, точнее, ваши пользователи) не получите соответствующей лицензии, он может предоставить контейнеру ключ, позволяющий тому создавать экземпляры элемента. Поскольку ключ сохраняется в приложении и невидимо используется им, контейнер сможет создавать экземпляры элемента для своих целей даже в том случае, если пользователь приложения не имеет на это права. Природа всех лицензионных ключей и семантика схемы лицензирования целиком оставляется на усмотрение элемента. ControlWizard, входящий в комплект Visual C++, при помощи MFC реализует простую схему, которую элемент может переопределить так, как считает нужным.
3.30 Регистрация Все COM-объекты должны быть занесены в системный реестр прежде, чем приложения смогут пользоваться ими. В реестре COM находит сведения о том, где найти выполняемый файл или DLL-библиотеку, необходимые для создания экземпляров объекта. Выполняемые файлы обычно автоматически регистрируют себя при запуске. Для элементов ActiveX, оформленных в виде
www.books-shop.com
DLL-библиотек, а также всех остальных внутрипроцессных серверов, это невозможно, поскольку они не могут работать вне контекста приложе-нияклиента. Соответственно, такие элементы должны быть зарегистрированы программой установки или контейнерами, которые предоставляют своим пользователям средства просмотра. При помощи таких средств пользователь сможет найти незарегистрированный элемент ActiveX и потребовать у контейнера вызвать функцию DLL-библиотеки, содержащей элемент, которая произведет регистрацию элемента. Затем элемент может быть использован контейнером (а также другими контейнерами) в качестве COM-объекта. Чтобы эта схема сработала, DLL-библиотеки, поддерживающие средства регистрации, должны реализовать COM-протокол саморегистрации. Для этого они должны экспортировать функцию DllRegisterServer. Также должна экспортироваться функция DllUnregisterServer, которая удаляет сведения о DLL из реестра. Контейнеры и программы просмотра должны уметь определять, поддерживает ли данная DLL-библиотека эти точки входа, не загружая ее, поскольку загрузка DLL-библиотеки может привести к нежелательным побочным эффектам и притом занимает относительно много времени. Спецификация Элементов ActiveX дополняет стандартную информацию о версии, чтобы по ней можно было определить факт поддержки саморегистрации. Дополнение выглядит очень просто: в категории StringFieldInfo требуется наличие нового ключа OLESelfRegister. Если эта строка присутствует (значение роли не играет — важен лишь факт ее наличия), то DLL-библиотека считается саморегистрирующейся. Кроме того, DLL-библиотека должна удовлетворять текущим требованиям COM и экспортировать функцию DllGetClassObject, которая функционально эквивалентна CoCreateInstance и запрашивает IID_CLASSFACTORY. Интересно заметить, что с помощью той же методики можно пометить как саморегистрирующиеся и выполняемые файлы. Вместо того, чтобы экспортировать функции, саморегистрирующиеся выполняемые файлы принимают ключи /REGISTER и /UNREGISTER в качестве аргументов командной строки. Хотя это и не является обязательным требованием, файлы элементов ActiveX обычно имеют расширение OCX. Стандартное расширение упрощает идентификацию файлов пользователями и их выбор в программах просмотра и контейнерах.
3.31 Некоторые ключи реестра Элементы ActiveX, как и все COM-объекты, сообщают пользователям информацию о себе при помощи ключей реестра. Некоторые из этих ключей являются стандартными и используются традиционными OLE-серверами. Другие ключи появились специально для элементов. Наибольший интерес представляют ключи Insertable, Control, DefaultIcon и ToolboxBitmap. Все они находятся в категории HKEY_CLASSES_ROOT\CLSID\{…}. С ключами Insertable и Control не связываются никакие значения — это просто пустые ключи реестра. Наличие таких ключей означает, то элемент обладает соответствующим атрибутом. Ключ Insertable свидетельствует о том, что элемент должен присутствовать в диалоговом окне Insert Object, отображаемом контейнерами. Стандартное для OLE диалоговое окно Insert Object (а также диалоговые окна, используемые приложениями) просматривает реестр в поисках объектов, для которых определен ключ Insertable. Затем найденные серверы отображаются в диалоговом окне. Отсутствие ключа Insertable еще не означает, что объект нельзя внедрить; оно говорит лишь о том, что объект не будет присутствовать в окне Insert Object. Иногда присутствие ключа Insertable может быть нежелательно — например, если объект должен внедряться лишь в контейнер, знающий об его существовании, или если разработчик не хочет, чтобы объект мог быть внедрен в любой контейнер.
www.books-shop.com
У элементов ключ Insertable часто отсутствует, поскольку обычно они используются не в любых контейнерах, а лишь в тех, которые ориентированы на работу с элементами. Ключ Control сообщает контейнеру о том, что объект представляет собой элемент ActiveX. Он также предназначен в основном для заполнения диалоговых окон. Новое поколение контейнеров, умеющих работать с элементами ActiveX, обычно отличает их от других внедряемых объектов и разрешает пользователю выбирать любую комбинацию внедряемых объектов и элементов. Диалоговые окна, в которых должны выводиться только элементы ActiveX, на основании этого ключа могут определить, следует ли им отображать тот или иной объект. Обычно элементы ActiveX помечаются не ключом Insertable, а ключом Control. Это означает, что они будут «невидимы» для пользователей контейнеров, написанных до появления спецификации Элементов ActiveX, хотя в некоторых случаях они могут быть успешно внедрены (присутствие или отсутствие ключа Insertable не сказывается на возможности внедрения элемента!). Тем не менее использование некоторых элементов может ограничиваться определенным контейнером, а другие элементы оказываются бесполезными в том случае, если контейнер не умеет обрабатывать события элемента. В первом случае оба ключа должны отсутствовать. Во втором случае ключ Control должен присутствовать, а ключ Insertable — отсутствовать. С появлением компонентных категорий ключ Insertable был заменен соответствующей категорией, а ключ Control объявлен устаревшим. Тем не менее пока все элементы и контейнеры не перешли на работу с категориями, занесенные в реестр ключи не принесут никакого вреда. Другой стандартный ключ OLE — DefaultIcon. Если объект отображается в виде значка, контейнер при помощи этого ключа выбирает, какой именно значок ему следует отобразить. Ключ DefaultIcon содержит имя выполняемого файла или DLL-библиотеки, содержащей значок, а также идентификатор ресурса для значка. Эта информация может быть использована при вызове функции Windows API ExtractIcon. Последний ключ, представляющий для нас интерес, появился в спецификации Элементов ActiveX: ToolboxBitmap32 (или ToolboxBitmap для 16-разрядных элементов). При регистрации элемента в некоторых контейнерах (например, Visual Basic) в палитре инструментов контейнера появляется его условное изображение. Разумеется, стандартные значки Windows слишком велики для этого, поэтому элементы должны содержать специальное растровое изображение для палитры. В настоящий момент необходимо, чтобы это растровое изображение имело размер 15ґ16 пикселей. В этом ключе содержится та же информация, что и для DefaultIcon — выполняемый файл или DLL-библиотека с растровым изображением и идентификатор ресурса.
3.32 Обновление версий объектов До появления спецификации Элементов ActiveX в COM существовал механизм, который позволял организовать обновление объектов без ущерба для их пользователей. Допустим, вы внедрили объект в контейнер и сохранили его. Позднее, перед повторной активизацией объекта, на компьютере была установлена обновленная версия сервера данного объекта. Если новый сервер выполняет правила, определенные в COM, то он сможет без всяких затруднений работать со старым объектом. Существуют два режима такой работы: эмуляция и преобразование. «Эмуляция» означает, что новая версия сервера имитирует работу старой версии. Новая версия может узнать формат хранения старого объекта и
записать любые изменения, внесенные в объект, в старом формате. «Преобразование» означает, что сервер обновляет объект до новой версии, следовательно, он понимает старый формат хранения объекта, но заменяет его новым. Выбор между эмуляцией и преобразованием в большинстве случаев осуществляется при сохранении объекта. При сохранении объекта COM включает в сохраняемую информацию флаг, который определяет операцию, выполняемую при загрузке данного объекта обновленной версией сервера. Впрочем, некоторые контейнеры и/или объекты также позволяют предоставить право выбора пользователю во время выполнения программы. Если сервер объекта решает поддерживать старые версии (несомненно, его стоит наделить такой возможностью), он регистрируется с новым CLSID, но добавляет в реестр специальные служебные ссылки при помощи функций COM и OLE API CoSetTreatAsClass и OleSetAutoConvert. Попытки вызова объекта через старый CLSID в тайне от пользователя приведут к вызову новой версии. Этот механизм успешно работает, поскольку внедрение объектов OLE основано на сущности интерфейсов как неизменных контрактов между объектом и его пользователем, а также на том, что оно не пользуется интерфейсами, относящимися к конкретному объекту. Кроме того, он полагается на сохранение объектом своего устойчивого состояния через интерфейс IStorage, поскольку при этом COM включает в объект сведения о том, следует ли ему осуществлять эмуляцию или преобразование. Как ни прискорбно, элементы ActiveX нарушают эти условия. Дело в том, что они раскрывают интерфейсы, к которым привязывается контейнер. Например, при загрузке элемента в контейнер, последний читает содержимое библиотеки типов элемента и получает из нее конкретный интерфейс Automation для свойств и методов, а также описание интерфейса событий. Эти интерфейсы также представляют собой контракты, однако они специфичны для данного объекта и не являются полиморфными. Припомните раздел «События» этой главы и рассмотренный нами элемент, вспомогательный класс которого раскрывает два интерфейса: IHexocx и IHexocxEvents. Элемент реализует первый из них как стандартный интерфейс Automation для свойств и методов. Второй интерфейс реализуется контейнером, чтобы элемент мог возбуждать события. Оба интерфейса являются специфичными для класса IHexocx. Кроме того, для обеспечения устойчивости большинство элементов ActiveX использует не только интерфейс IStorage. Многие из них поддерживают другие интерфейсы (IStreamInit) и механизм сохранения в текстовом формате через комплекты свойств или через наборы свойств OLE посредством IData Object. Вероятно, новая версия элемента отличается от старой не только исправленными ошибками — скорее всего, она обладает новыми функциями, а это может означать изменения и дополнения в существующих интерфейсах. От неизменности контракта не остается и следа! К счастью, существуют различные сценарии, которые обычно помогают справиться с этой неприятной проблемой. Такими сценариями являются: двоичная совместимость (binary compatibility), совместимость на уровне исходного текста (source compatibility) и их сочетание. «Двоичная совместимость» элемента с его более ранней версией означает, что его можно подключить к существующему приложению, спроектированному и построенному с использованием старой версии, и при этом приложение будет работать точно так же, как раньше. «Совместимость на уровне исходного текста» означает, что новая версия элемента продолжает без изменений работать с пользовательским кодом в
www.books-shop.com
приложении-контейнере, однако из-за изменившихся внутренних идентификаторов, имен и т. д. программу пользователя необходимо построить заново. Рассмотрим программу на Visual Basic, которая рассчитана на работу с версией 1.0 нашего элемента. Появляется версия 1.1, пользователь покупает ее и устанавливает на своем компьютере. Если новая версия обладает двоичной совместимостью, то его программа продолжает нормально работать. Если же совместимость достигается только на уровне исходного текста, то программу придется предварительно перекомпилировать, но после этого она будет работать без дальнейших модификаций. Обычно разработчики элементов стараются обеспечить оба вида совместимости своих продуктов с предыдущими версиями. Для обеспечения двоичной совместимости элемент с точки зрения контейнера не должен ничем отличаться от старой версии. Он должен поддерживать старый CLSID, обладать точно такими же интерфейсами (то есть имеющими те же IID, имена, dispid, параметры и т. д.) и предоставлять точки соединения, принимающие те же IID. Конечно, это не означает, что при необходимости элемент не может поддерживать совершенно новый CLSID. Разумеется, он также может обладать новыми интерфейсами в дополнение к старым. Более тонкий момент заключается в том, что интерфейсы Automation можно расширить так, чтобы с точки зрения приложения они оставались теми же. Например, в интерфейс Automation можно добавить новые методы и свойства, если их dispid не использовались в старой версии. Аналогично, новые методы можно добавлять и в не-диспетчирующие интерфейсы, при условии, что при этом не изменяется порядок и расположение старых методов в v-таблице (тем не менее в этом случае вы фактически создаете новый интерфейс, производный от старого, так что его следует оформить как новый интерфейс). Элемент также может принимать вызовы QueryInterface с IID старых интерфейсов и возвращать указатели на новые интерфейсы, если они способны полностью выполнять старые контракты. Добиться совместимости для точек соединения несколько сложнее, поскольку во время вызова Advise точка соединения запрашивает интерфейс приемника посредством вызова QueryInterface для некоторого IID. Если новая версия элемента будет запрашивать новый IID, контейнер не сможет предоставить его, поскольку он написан в расчете на запрос старого IID. Следовательно, если вызов нового IID завершается неудачей, элемент должен вызвать Query Interface для старого IID. Чтобы обеспечить совместимость на уровне исходного текста, элемент должен поддерживать все старые интерфейсы с теми же свойствами, методами и параметрами. Интерфейсы могут изменяться в других отношениях, а элемент может обладать новым CLSID (это стоит сделать, если только вы при этом не добиваетесь двоичной совместимости), однако для работы с новой версией исходный текст программы изменять не придется. Тем не менее в некоторых ситуациях в новой версии элемента просто невозможно обеспечить какой-либо из описанных уровней совместимости. В этом случае новая версия должна во всех отношениях рассматриваться как новый элемент.
ЗАМЕЧАНИЕ Некоторые практические аспекты, относящиеся к обновлению версий и в особенности к работе с устойчивыми свойствами в этой ситуации, рассмотрены в главе 10, «Консолидация».
www.books-shop.com
3.33 Спецификация OCX 96 Спецификация Элементов OLE 94 стала заметным шагом вперед по сравнению с VBX. Элементы, созданные с ее помощью, можно было переносить между различными контейнерами и преобразовывать в 32разрядный вид, их функциональность была достаточно широкой. Тем не менее в некоторых ситуациях и они вели себя не идеально. Например, давайте представим себе экранную форму, содержащую десятки и даже сотни элементов. Если все они будут реализованы как элементы OLE, то каждый элемент придется загружать и инициализировать как самостоятельный объект OLE c возможностью активизации на месте. Элементы, немедленно отображаемые на экране, обычно сразу же активизируются (а для этого приходится создать окно элемента), так что рабочая среда (объем используемой памяти) оказывается чрезмерно большой. Все перечисленное, вместе с другими встречающимися там и сям мелкими недочетами, заметно сказывается на показателе, который еще никогда и никому не удавалось довести до идеала — на производительности программы. Большинство существующих реализаций элементов также не особенно заботились о производительности (считалось, что главное — это простота использования!). Из всего сказанного становится ясно, почему производительность элементов OLE в лучшем случае оценивалась как посредственная. Несколько рабочих групп внутри Microsoft начали работать над улучшением спецификации. Многие участники в большей или меньшей степени занимались разработкой исходной архитектуры элементов OLE, поэтому они обладали замечательным опытом для дальнейшей работы. Ведущая роль принадлежала команде экранных форм, разработавшей универсальный комплект форм для различных приложений Microsoft; со временем он превратится в отдельный пакет, который может использоваться всеми разработчиками. Эта команда критически проанализировала архитектуру Элементов OLE и предложила новую спецификацию, которая позже получила название OCX 96. Кроме того, участники команды направили особые усилия на повышение производительности. Изменяя требования, предъявляемые к элементу и его контейнеру, они добивались многочисленных улучшений. Объединив усилия с другими группами (среди которых была группа, отвечавшая за интеграцию OLE на уровне операционной системы), им удалось существенно улучшить производительность элементов. Вопросами производительности также занималась команда MFC, создавшая первый общедоступный пакет для разработки элементов OLE (речь идет об ушедшем в прошлое OLE CDK), и команда Visual Basic, которая в первую очередь старалась по возможности сократить время загрузки и отображения экранных форм Visual Basic. Первые усовершенствования были внесены командой MFC в библиотеку MFC версий 4.0 и выше, и созданные при ее помощи элементы работали намного быстрее своих предшественников. Команда Visual Basic разработала новый шаблон для создания элементов, в котором основное внимание уделялось производительности, а простота использования была отчасти принесена в жертву поставленной цели. Их работа впервые увидела свет в виде Win32 BaseCtl — шаблона C++ для разработки быстрых элементов, который обходился без назойливой опеки MFC. Наибольший интерес для нас представляют изменения в архитектуре элементов. В этом разделе главы мы посмотрим, что же такое OCX 96.
3.34 Активизация Большая часть элементов (включая те, что были построены при помощи Visual C++ ControlWizard) по умолчанию помечаются как «активизируемые при отображении». Это означает, что при загрузке объекта, содержащего один или несколько элементов, отображаемые на экране элементы должны немедленно переводиться в активное состояние. Такая процедура занимает
www.books-shop.com
относительно много времени. Работая над повышением производительности, участники команд постарались выяснить, действительно ли необходимо помечать элементы подобным образом. Если бы удалось сэкономить на активизации, то формы отображались бы значительно быстрее и не предъявляли завышенных требований к размерам рабочей среды. Наверное, по такому вступлению вы уже догадались, что элементы не всегда должны активизироваться при отображении. Тем не менее находясь в неактивном состоянии, они должны располагать средствами для общения с пользователем. Например, такой элемент может выступать в качестве приемника при операциях drag-and-drop или иным способом взаимодействовать с мышью. Неактивный элемент не может сделать этого, поскольку он не имеет своего окна. В OCX 96 такую возможность предоставляет интерфейс IPointerInactive, который выглядит следующим образом:
IPointerInactive : IUnknown { HRESULT GetActivationPolicy(DWORD *pPolicy); HRESULT OnInactiveMouseMove(LPRECT lprcBounds, LONG x, LONG y, DWORD grfKeyState); HRESULT OnInactiveSetCursor(LPRECT lprcBounds, LONG x, LONG y, DWORD dwMouseMsg, BOOL fSetAlways); } Как нетрудно представить, взаимодействие неактивных элементов с пользователем организовано большей частью контейнером. В основном он занимается тем, что через интерфейс IPointerInactive передает элементу большую часть предназначенных для него сообщений о перемещении мыши. Если контейнер получает сообщение WM_SETCURSOR или WM_MOUSEMOVE, а курсор в этот момент находится над неактивным элементом, то контейнер должен обратиться к данному элементу. Разумеется, при этом возникают некоторые проблемы. Каким образом контейнер узнает, что тот или иной элемент неактивен и что он поддерживает интерфейс IPointerInactive? А если этого мало, то каким образом элементы OCX 96, поддерживающие этот интерфейс, должны работать со старыми контейнерами, не поддерживающими работу с неактивными элементами? Ответ на первый вопрос соответствует лучшим традициям COM — во время загрузки элемента контейнером последний проверяет бит OLEMISC_ACTIVATEWHENVISIBLE и наличие/отсутствие интерфейса IPointerInactive (вызывая для него QueryInterface). Если контейнер обнаруживает, что курсор мыши находится над неактивным элементом (да, в подобных случаях контейнеру приходится заниматься проверкой координат курсора) и данный элемент поддерживает IPointerInactive, то контейнер обладает большей частью необходимой информации. Выражение «большей частью» будет объяснено в следующем абзаце, а пока мне хочется ответить на вопрос о том, как организовать работу элемента в «устаревшем» контейнере, не умеющем обращаться с неактивными элементами. Прежде всего, если элемент должен работать в таком контейнере, для него следует установить флаг OLEMISC_ACTIVATEWHENVISIBLE, чтобы контейнер мог активизировать его обычным образом. Кроме того, для всех таких элементов необходимо устанавливать флаг OLEMISC_IGNOREACTIVATEWHENVISIBLE, чтобы они могли успешно работать в неактивном состоянии. Новый контейнер, умеющий работать с элементами OCX 96, увидит оба бита и будет знать, что бит активизации следует игнорировать. Таким образом, один из флагов сообщает заведомо ложную информацию — но эта ложь во благо! Теперь давайте вернемся к выражению «большей частью». Когда контейнер, работающий с неактивными элементами, определяет, что курсор мыши вошел
www.books-shop.com
в область неактивного элемента, удовлетворяющего спецификации OCX 96, он должен вызвать метод IPointerInactive::GetActivationPolicy данного элемента. Возвращаемое методом значение представляет собой набор флагов, объединенных при помощи операции логического ИЛИ. Обратите внимание на то, что кэширование в данном случае недопустимо — контейнер должен обращаться к элементу каждый раз, когда у него возникнет такая необходимость (то есть когда курсор мыши входит в область окна, занятую неактивным элементом). Это позволяет элементу по-разному реагировать на такое обращение в зависимости от обстоятельств. В настоящее время поддерживаются следующие флаги:
POINTERINACTIVE_ACTIVATEONENTRY
Флаг сообщает контейнеру о том, что элемент должен быть активизирован на месте в тот момент, когда курсор мыши окажется над ним. Это позволяет контейнеру реагировать на другие сообщения мыши способами, традиционными для спецификации Элементов OLE 94. Обычно этот флаг устанавливается для элементов, действия которых не ограничиваются возбуждением событий мыши или изменением курсора. Сразу же после активизации элемента ему немедленно передается сообщение о перемещении мыши, которое заставило контейнер вызвать GetActivationPolicy и получить этот флаг. В дальнейшем элемент может обратиться с запросом на деактивизацию; см. описание следующего флага.
Флаг отчасти напоминает предыдущий, за исключением того,что он не может быть возвращен отдельно от других, а предыдущий — может. Если для элемента установлен только предыдущий флаг, то он будет POINTERINACTIVE_DEACTIVATEONLEAVE оставаться активным. Если же для него установлены оба флага, то контейнер деактивизирует элемент в тот момент, когда курсор мыши выходит за пределы элемента (то есть когда контейнер получит следующее сообщение от мыши). POINTERINACTIVE_ACTIVATEONDRAG
Флаг относится к операции dragand-drop с неактивным элементом. Drag-and-drop рассматривается ниже.
Метод IPointerInactive::OnInactiveSetCursor вызывается контейнером при каждом сообщении WM_SETCURSOR, которое он получает, когда курсор мыши находится над неактивным элементом, поддерживающим этот интерфейс.
ЗАМЕЧАНИЕ В отличие от методов обычных внедренных объектов, методы интерфейса IPointerInactive
www.books-shop.com
пользуются значениями координат в пикселях, а не в единицах HIMETRIC. Это заметно упрощает ситуацию, поскольку большинство программистов для Windows привыкло к такому способу измерения координат.
При помощи параметра lprcBounds элемент узнает свой размер и положение, а параметры x и y определяют координаты курсора мыши во времянаступления события (опять же в клиентских координатах). Параметр dwMouseMsg содержит значение lParam для исходного сообщения WM_SETCURSOR. Этот метод позволяет элементу изменить вид курсора мыши, находящегося над ним, но на этом его возможности заканчиваются. Если последний параметр метода, fSetAlways, равен TRUE, то контейнер сообщает элементу о том, что он должен задать вид курсора. Если параметр равен FALSE, то элемент не обязан задавать вид курсора, хотя может это сделать при желании. Если элемент задал вид курсора, метод должен возвратить S_OK, а в противном случае — S_FALSE. Метод OnInactiveSetCursor отчасти похож на предыдущий, он вызывается контейнером при поступлении сообщения WM_MOUSEMOVE во время нахождения курсора над неактивным элементом. Его параметры также описывают прямоугольник, внутри которого находится элемент, и координаты x и y курсора мыши. Четвертый параметр, grfGetKeyState, сообщает элементу о текущем состоянии «специальных» клавиш (таких, как Ctrl или Shift) и кнопок мыши.
3.35 Drag-and-drop для неактивных элементов Операция drag-and-drop в OLE основана на том, что приемник (объект, реагирующий на «сбрасывание») раскрывает интерфейс IDropTarget. Для работы этого интерфейса объект должен иметь окно, при помощи которого интерфейс мог бы зарегистрировать свое присутствие. Разумеется, неактивные элементы не удовлетворяют этому условию. Спецификации OCX 96 удалось избежать чрезмерного усложнения поддержки drag-and-drop в неактивном состоянии. В соответствии с ней события развиваются следующим образом:
Если во время вызова методов контейнера IDropTarget::DragOver или IDropTarget::DragEnter контейнер обнаруживает, что мышь находится над неактивным объектом, он должен вызвать QueryInterface для интерфейса IPointerInactive данного объекта. Если QueryInterface не возвращает интерфейсный указатель, считается, что объект не может быть приемником. Если интерфейс поддерживается объектом, то контейнер вызывает его метод GetActivationPolicy. Если возвращаемое значение включает флаг POINTERINACTIVE_ACTIVATEONDRAG, контейнер активизирует объект на месте. Элемент регистрирует свой собственный интерфейс IDropTarget для созданного окна, и операция drag-and-drop продолжается с этим интерфейсным указателем. Поскольку в момент начала всех этих событий контейнер выполнял метод DragEnter или DragOver, возвращаемое этим методом значение pdwEffect должно быть равно DROPEFFECT_NONE. OLE обнаруживает, что окно операции drag-and-drop изменилось, вследствие чего вызывает метод контейнера IDropTarget::DragLeave и метод элемента IDropTarget::DragEnter. Если перетаскиваемый объект будет сброшен на элемент, происходит UI-активизация последнего, с последующей UI-деактивизацией в момент утраты фокуса. Если сбрасывания не было, контейнер должен UI-деактивизировать элемент при следующем вызове его метода IDropTarget::DragEnter (то
www.books-shop.com
есть когда курсор мыши снова переместится с элемента на контейнер). Как видно из описания, неактивность элементов заметно усложняет работу, которую должен проделать контейнер, поддерживающий drag-and-drop.
ЗАМЕЧАНИЕ Некоторые из описанных выше правил слегка изменяются для внеоконных элементов, поскольку такие элементы не обладают окнами, способными выполнить необходимые операции. Изменения будут рассмотрены в следующем разделе.
3.36 Внеоконные элементы Следующий крупный выигрыш в производительности элементов был достигнут благодаря предложению команды экранных форм — ввести категорию элементов, для которых вообще не создаются никакие окна (во всяком случае, при использовании их в новых контейнерах, поддерживающих концепцию внеоконных элементов — в старых контейнерах внеоконным элементам приходится полагаться на прежние, оконные механизмы UIактивизации). Отсутствие окна заметно снижает объем рабочей среды и время загрузки элемента. Внеоконные элементы обладают и другими достоинствами — например, они могут иметь произвольную форму (окна Windows должны быть прямоугольными). Кроме того, такие элементы могут быть прозрачными, что открывает перед разработчиком многие нестандартные возможности. Например, вы можете создать элемент, состоящий из статического текста на прозрачном фоне, сквозь который будет видно все, что находится позади элемента. Добиться того же эффекта при помощи элементов, отображающихся в собственных окнах, было бы крайне сложно (или даже невозможно). Большая часть действий, предпринимаемых типичным элементом, не требует наличия собственного окна, хотя с первого взгляда может показаться противоположное. Часть спецификации OCX 96, посвященная внеоконным элементам, описывает принципы их работы и то, как потребности элементов в оконном выводе могут быть удовлетворены всего одним совместным окном, которое предоставляется контейнером всем его элементам (или лишь тем, которые находятся на данной форме). Во время знакомства со спецификацией OCX 96 меня заинтересовало то внимание, которое ее разработчики уделяли размеру и скорости работы элементов. Например, многие новые интерфейсы внеоконных элементов представляют собой расширения соответствующих оконных прототипов, причем расширение осуществляется посредством наследования. Это означает, что для целого набора взаимосвязанных интерфейсов существует всего одна реализация IUnknown, что приводит к уменьшению v-таблицы на три элемента и к соответствующему сокращению количества указателей на vтаблицы в каждом объекте, поскольку для каждой группы производных интерфейсов будет достаточно всего одного указателя на v-таблицу. Первое требование к элементу — компактность и быстрота, поэтому вполне естественно, что существенная часть работы внеоконного элемента возлагается на его контейнер. Чтобы понять «идеологию» работы внеоконного элемента, лучше всего представить себе окно контейнера «исполняющим обязанности» окна элемента, причем доставка всех важных сообщений от этого окна к элементу должна осуществляться контейнером. При таком подходе элементу удается пользоваться всеми преимуществами окна, избегая связанных с этим расходов.
www.books-shop.com
Все сказанное означает, что на концептуальном уровне внеоконный элемент не так уж сильно отличается от оконного — он должен поддерживать состояния активности OLE и по указанию контейнера переходить в состояние активности на месте и UI-активности. Конечно, при этом между двумя видами элементов существуют достаточно тонкие различия. Ведущую роль для внеоконных элементов играют два интерфейса — IOleInPlaceObjectWindowless (производный от IOleInPlaceObject) и IOleInPlaceSiteWindowless (производный от IOleInPlaceSiteEx, который, в свою очередь, является производным от IOleInPlaceSite и используется некоторыми средствами оптимизации вывода OCX 96 наряду с внеоконными элементами). Чтобы внеоконный элемент мог определить, способен ли он на внеоконную активизацию, он должен сначала вызвать QueryInterface своего узла для интерфейса IOleInPlaceSiteWindowless. Если интерфейс существует, то элемент запрашивает, допускается ли его внеоконная активизация, для чего он вызывает метод CanWindowlessActivate этого интерфейса. Если QueryInterface завершается неудачей или метод отвечает отрицательно (возвращая S_FALSE), элемент должен вернуться к обычной, оконной активизации и работать как обычный элемент OCX 94. Разумеется, чтобы эта схема работала, контейнер должен быть написан с учетом поддержки внеоконных элементов, в противном случае активизация происходит, как обычно. Как контейнер узнает, что его элемент является внеоконным? Вообще говоря, он может вызвать метод IOleInPlaceActiveObject::GetWindow, который обычно возвращает логический номер окна, используемого элементом. Внеоконные элементы должны возвращать S_FALSE, показывая тем самым, что объект не связан ни с каким окном. Тем не менее OLE не запрещает обычным, оконным объектам откладывать создание окон до момента вызова IOleInPlaceSite::OnInPlaceActivateEx, так что нормальный оконный объект тоже может вернуть S_FALSE. Чтобы внеоконный объект мог сообщить, что действительно является внеоконным, он должен при активизации на месте вызвать метод IOleInPlaceSiteEx::OnInPlaceActivateEx, второй параметр которого содержит набор флагов. Пока в этом наборе определен всего один флаг ACTIVATE_WINDOWLESS, назначение которого понятно без комментариев. Контейнер может сохранить полученное значение для дальнейшего использования или вызывать этот метод каждый раз, когда ему снова потребуется получить ответ. Если учесть внимание, уделяемое скорости работы элементов, второй вариант выглядит более оправданным.
3.37 Получение сообщений внеоконным элементом Разумеется, внеоконный элемент не может получать оконных сообщений. Вместо этого контейнер вызывает метод OnWindowMessage интерфейса IOleInPlaceObjectWindowless, принадлежащего элементу. Этот метод напоминает стандартную функцию Win32 API SendMessage, за исключением того, что в нем не используется логический номер окна и он возвращает значение стандартного для Win32 типа LRESULT вместе со стандартным для COM HRESULT. Если элемент не желает обрабатывать сообщение и хочет, чтобы оно было обработано по умолчанию, он не должен напрямую вызывать DefWindowProc, поскольку в этом случае у контейнера не будет возможности обработать сообщение. Вместо этого он должен воспользоваться методом OnDefWindowMessage интерфейса IOleInPlaceSiteWindowless. Элемент может захватить курсор мыши, вызывая метод SetCapture интерфейса IOleInPlaceSiteWindowless (признаюсь, мне надоело набирать это длинное название, так что в дальнейшем я буду пользоваться сокращением IOIPSW). Если параметр этого метода равен TRUE, значит, элемент желает захватить курсор мыши (хотя контейнер может ему в этом отказать), если
www.books-shop.com
параметр равен FALSE, элемент отдает захваченный ранее курсор. Если курсор мыши захватывается внеоконным элементом, контейнер должен захватить курсор собственным окном, а затем передавать элементу все сообщения мыши. Элемент может определить, захватил ли он курсор мыши в данный момент, при помощи метода IOIPSW::GetCapture — возвращаемое значение S_OK свидетельствует о захвате. Еще один интересный аспект внеоконных объектов связан с фокусом ввода: если у объекта нет окна, как же ему получать ввод с клавиатуры? И снова на помощь приходят контейнеры, поддерживающие спецификацию OCX 96. Когда внеоконный элемент «получает фокус» (выражение взято в кавычки, поскольку речь идет о концептуальном, а не реальном фокусе — надеюсь, вы меня поняли), контейнер на самом деле должен получить фокус для своего окна и затем передавать объекту все сообщения от клавиатуры, а также другие сообщения для окна, обладающего фокусом, — например, WM_HELP или WM_CANCELMODE. Чтобы получить фокус, внеоконный объект не должен вызывать стандартную функцию Win32 API SetFocus, поскольку у него нет логического номера окна HWND, который можно было бы указать в качестве параметра. Вместо этого он вызывает метод IOIPSW::SetFocus и поручает всю работу контейнеру. Тот же метод применяется и для освобождения фокуса, однако на этот раз его параметр должен быть равен FALSE. Предполагается, что контейнер вызовет функцию Win32 API с параметром HWND, равным NULL, поэтому фокус не будет передан никакому конкретному объекту. Внеоконный объект определяет, захватил ли он курсор мыши, вызывая метод IOIPSW::GetFocus; если метод возвращает S_OK, курсор захвачен. Если элемент не хочет обрабатывать какое-либо сообщение от клавиатуры, он должен возвратить S_FALSE при вызове метода IOleInPlaceActiveObjectWindowless::OnWindowMessage, используемого контейнером для передачи сообщения элементу. После этого контейнер может вызвать DefWindowProc. Внеоконные объекты отслеживают нажатие своих акселераторов точно так же, как это делают стандартные объекты OLE, активизированные на месте, — посредством IOleInPlaceActiveObject::TranslateAccelerator. Обычно объект распознает акселератор, преобразует его в сообщение WM_COMMAND и затем посылает это сообщение себе самому. Эта схема хорошо работает, если у вас есть окно, но без окна она бесполезна. Следовательно, внеоконный объект в этом отношении отклоняется от нормы и просто немедленно обрабатывает команду. Кроме того, поскольку ввод с клавиатуры обычно поступает в окно контейнера (у объекта такого окна нет — я повторяю это снова и снова, чтобы подчеркнуть идею внеоконности), объект должен получить логический номер окна контейнера и использовать его при вызове метода TranslateAccelerator.
3.38 Графический вывод Вообще говоря, в мире внедренных объектов неактивные объекты рисуются контейнером посредством вызова IViewObject::Draw, тогда как активный объект, обладающий собственным окном, перерисовывается автоматически. Разумеется, для внеоконных элементов это не годится, потому контейнер заставляет любой, даже активизированный на месте внеоконный объект перерисовать себя, для чего вызывает метод IViewObject::Draw. Тем не менее существуют небольшие различия между элементами неактивными и элементами активными на месте, но внеоконными. Параметр lprcBounds метода Draw, который обычно задает объекту прямоугольник для рисования, во внеоконном случае будет равным NULL. Вместо него элемент должен пользоваться прямоугольником, полученным в ходе активизации на месте. Кроме того, в этом случае ограничивается диапазон аспектов, которые могут быть воспроизведены подобным образом: параметр dwAspect метода
www.books-shop.com
Draw может принимать значения только DVASPECT_CONTENT, DVASPECT_OPAQUE и DVASPECT_TRANSPARENT (два последних варианта подробно рассмотрены в разделе «Оптимизация графического вывода»). Наибольший интерес представляет параметр hdcDraw, определяющий логический номер контекста устройства (HDC), в котором должно происходить рисование. Контекст должен находиться в том состоянии, в котором он передается окну при его первом получении (например, в результате получения сообщения WM_PAINT). В частности, это означает, что контекст устройства должен находиться в координатном режиме MM_TEXT, а его логические координаты должны совпадать с клиентскими координатами окна контейнера. Если при вызове метода IViewObject::Draw параметр lprcBounds равен NULL в любой ситуации, за исключением рисования во внеоконном, активизированном на месте элементе, это считается ошибкой, и элемент должен вернуть E_INVALIDARG.
3.39 Внеоконные операции drag-and-drop Операция drag-and-drop тесно связана с окном, поэтому для обеспечения ее работы в условиях внеоконных элементов как элементу, так и контейнеру приходится проделать некоторую дополнительную работу. Механизм отчасти напоминает тот, что используется для неактивных объектов, но, конечно, для его реализации требуется больше усилий. Прежде всего элемент должен реализовать интерфейс IDropTarget, однако он не может зарегистрировать его в системе (поскольку не существует окна, с которым его можно было бы связать) и вообще включить его в «личность» объекта (то есть реализация QueryInterface элемента должна отрицать его существование и возвращать E_NOINTERFACE при запросе IID_IDropTarget). Контейнер также должен реализовать IDropTarget, однако он обязан зарегистрировать этот интерфейс в OLE. Затем, если во время вызова метода контейнера DragEnter или DragOver контейнер определяет, что курсор мыши находится над активизированным на месте внеоконным объектом, он должен вызвать метод элемента IOleInPlaceObjectWindowless::GetDropTarget, чтобы получить указатель на принадлежащий элементу интерфейс IDropTarget (если будет возвращено значение E_NOTIMPL, элемент не поддерживает drag-and-drop). Возвращенный интерфейсный указатель может быть сохранен контейнером для дальнейшего использования. Затем контейнер поручает дальнейшую обработку этому интерфейсу, вызывая его метод DragEnter и возвращая полученное от элемента значение pdwEffect в ответ на вызов DragEnter или DragOver. Когда курсор мыши покидает внеоконный элемент, контейнер должен вызвать DragLeave интерфейса IDropTarget элемента, чтобы известить его о выходе операции drag-and-drop за пределы элемента. После этого контейнер освобождает интерфейс. Элемент имеет право возвратить S_FALSE при вызове DragEnter — этим он сообщает, что не принимает данных ни в одном формате, представленном интерфейсом IDataObject источника drag-and-drop. В этом случае контейнер может принять данные вместо элемента. Для неактивных внеоконных элементов используется тот же механизм, что и для неактивных оконных элементов (см. выше; суть сводится к вызову GetActivationPolicy с последующей активизацией на месте в случае необходимости), за исключением того, что отсутствие окна даже при активизации на месте означает, что контейнеру придется вызывать GetDropTarget и описанный выше метод для того, чтобы элемент смог поддерживать drag-and-drop. В конце операции контейнер деактивизирует элемент.
Если оконный элемент пожелает перерисовать себя или изменить часть своего изображения независимо от требования контейнера, он может объявить недействительной часть своей экранной области, чтобы позднее получить сообщение WM_PAINT или же получить контекст устройства для рисования или каких-то вычислений, основанных на характеристиках контекста. Если элемент не имеет окна, процесс оказывается более сложным. Внеоконный элемент не может воспользоваться функциями Win32 API, в которых участвуют логические номера окна (например, функцией GetDC для получения контекста устройства), поэтому вместо этого ему приходиться пользоваться методами интерфейса IOleInPlaceSiteWindowless. Эти методы являются ключевыми для внеоконного рисования:
HRESULT HRESULT HRESULT HRESULT HRESULT
GetDC(LPRECT lpRect, DWORD dwFlags, HDC *phdc); ReleaseDC(HDC hdc); InvalidateRect(LPCRECT lprc, BOOL fErase); InvalidateRgn(HRGN hrgn, BOOL fErase); ScrollRect(int dx, int dy, PLCRECT lprcScroll, LPCRECT lprcClip); HRESULT AdjustRect(LPRECTL lprc); Как нетрудно убедиться, большая часть этих методов повторяют функции Win32 API. Метод GetDC используется элементом для получения контекста устройства, а ReleaseDC освобождает контекст после его использования. Некоторые параметры GetDC довольно интересны. Первый из них, lpRect, описывает прямоугольник, в котором желает рисовать объект (NULL означает весь объект), заданный в клиентских координатах окна-владельца. Перед тем как возвращать контекст устройства в результате этого вызова, контейнер должен правильно задать область отсечения (clipping region), чтобы элемент не смог случайно нарисовать что-нибудь за пределами своей части окна. Второй параметр, dwFlags, сообщает методу GetDC, что элемент собирается делать с полученным контекстом. Значение 0 означает, что элемент намерен выполнять с ним операции, обычные при работе с контекстом устройства. Если элемент не собирается рисовать и планирует воспользоваться контекстом для вычислений (например, определения текстовых метрик), он должен передать значение OLEDC_NODRAW. Передавая OLEDC_PAINTBKGND, он требует от контейнера перерисовать фоновую область за элементом перед тем, как возвращать контекст устройства, этот параметр используется прозрачными объектами (см. раздел «Оптимизация графического вывода»). Значение OLEDC_OFFSCREEN сообщает контейнеру, что элемент хотел бы получить совместимый контекст устройства (memory device context), чтобы подготовить изображение за пределами экрана. Контейнер может отказать элементу в этой просьбе, хотя он обязан вернуть экранный контекст устройства, если этот флаг не установлен. Как и при обычном программировании для Windows, после завершения работы с контекстом устройства, полученным при помощи IOIPSW::GetDC, внеоконный элемент обязан освободить его методом IOIPSW::ReleaseDC. Чтобы объявить недействительной некоторую часть своей области окна (прямоугольную или произвольной формы), элемент может воспользоваться методами IOIPSW::InvalidateRect и IOIPSW::InvalidateRgn. Аналогичные методы присутствуют и в интерфейсе IAdviseSinkEx (см. ниже), однако они относятся к стандартному случаю и получают координаты в единицах HIMETRIC, тогда как методы IOIPSW работают с клиентскими координатами. Оба метода получают флаг fErase, при помощи которого элемент требует у контейнера стереть (TRUE) или не стирать (FALSE) фон обновляемой области. Прокрутка (scrolling) также осуществляется через интерфейс IOIPSW, а не через стандартный Windows API — отчасти из-за отсутствия окна, но также и из-за того, что прозрачные объекты способны существенно усложнить операцию прокрутки. Поскольку внеоконные элементы могут быть
www.books-shop.com
прозрачными, для выполнения простейшей операции прокрутки контейнеру приходится выполнить большой объем работы. Элемент делает свою часть работы без особых затруднений — он просто вызывает метод IOIPSW::ScrollRect с соответствующими параметрами. Затем контейнер, в зависимости от расположения перекрывающихся объектов и сложности своего кода рисования, либо просто перерисовывает нужный прямоугольник, либо выполняет серию операций отсечения (clipping) и рисования для перекрывающихся объектов. Элемент обо всем этом ничего не знает — он лишь говорит «Прокрути меня», а контейнер повинуется. Метод ScrollRect получает четыре параметра:
lprcScroll — прокручиваемый прямоугольник в клиентских координатах (окна контейнера); значение NULL означает прокрутку всего объекта. lprcClip — перерисовываемый прямоугольник (NULL означает весь прямоугольник lprcScroll). dx и dy — количество пикселей, прокручиваемых по осям x и y.
Наконец, если элемент хочет вывести текстовый курсор (каретку), он должен сначала проверить, не помешает ли это перекрывающим окнам. Разумеется, сам он этого сделать не может и потому обращается за помощью к контейнеру. Используемый для этого метод AdjustRect на самом деле решает более общую задачу — он просто проверяет заданный прямоугольник на наличие перекрывающих непрозрачных объектов и в случае необходимости «подрезает» его. Он возвращает S_OK, если прямоугольник виден целиком, и S_FALSE, если получившийся прямоугольник имеет нулевую ширину или высоту из-за полного перекрытия.
3.40 Оптимизация графического вывода Среди прочих изменений в спецификации OCX 96 появилось «трехмерное» рисование — элементы получили возможность перекрывать другие элементы. Впрочем, это не совсем верно, поскольку один элемент может располагаться поверх другого и без OCX 96. Однако OCX 96 позволяет сделать элемент прозрачным, чтобы сквозь него был виден находящийся позади фон. В некоторых обстоятельствах (по желанию разработчика элемента) эти прозрачные участки могут считаться «несуществующими», чтобы щелчок на прозрачной части элемента рассматривался находящимся сзади объектом, а не прозрачным элементом. Чтобы новая схема могла эффективно работать, были внесены некоторые исправления, которые устранили мерцание при перерисовке элементов и позволили элементам самим определять координаты точки щелчка. Кроме того, были приложены усилия к общему повышению скорости графического вывода.
3.41 Усовершенствование метода IViewObject::Draw Сначала мы рассмотрим, какие меры были приняты для повышения общей производительности графического вывода. Получаемый контекст устройства обладает набором объектов GDI (компонента Windows, отвечающего за работу с графикой) — кистей, перьев и шрифтов, выбранных в данном контексте. Если вы захотите рисовать другой кистью или пером или же воспользоваться другим шрифтом, необходимо создать или получить нужный объект и выбрать его в контексте устройства. В конце рисования, перед освобождением контекста устройства, необходимо снова выбрать в нем старые объекты. Эта работа требует немало времени. Некоторые объекты на форме могут использовать одни и те же атрибуты перьев, кистей и шрифтов, поэтому отказ от этих выборов/восстановлений мог бы увеличить производительность при воспроизведении сразу нескольких объектов.
www.books-shop.com
Спецификация OCX 96 вносит изменения в метод IViewObject::Draw (не нарушая существующего контракта!) и позволяет в некоторых случаях обойтись без выбора объектов GDI в контексте устройства. Метод IViewObject::Draw имеет параметр pvAspect, ранее считавшийся зарезервированным, который в обычном сервисе OLE должен иметь значение NULL. Теперь этот параметр может содержать указатель на структуру DVASPECTINFO, содержащую два элемента: количество байт в структуре и набор флагов. В настоящее время допускается только флаг DVASPECTINFOFLAG_CANOPTIMIZE. При установке контейнером этого флага в структуре DVASPECTINFO, передаваемой IViewObject::Draw, элемент может оставить объекты шрифта, кисти и пера, выбранные в текущем контексте устройства. Кроме того, может быть оставлен выбранным любой другой атрибут контекста устройства, который может быть сброшен другим элементом — например, цвет фона или текста и т. д. Атрибуты, которые не могут быть восстановлены (например, область отсечения или выбранный в контексте растр), не могут оставаться выбранными. Только элемент, который выбрал данный объект GDI в контексте устройства, имеет право удалить его. Следовательно, обычно элемент хранит логические номера используемых им объектов GDI, удаляя или заменяя их, когда ему потребуется работать с другим объектом. Разумеется, работу с объектами GDI можно ускорить посредством кэширования логических номеров (что и делается в стандартном шрифтовом объекте).
3.42 Активизация без мерцания Каждый раз, когда объект активизируется на месте, он перерисовывает себя. Во время деактивизации объекта контейнер также перерисовывает его. В этой схеме нет ничего плохого, если внедрен только один большой объект, но при большом количестве элементов на форме постоянное перерисовывание вызывает мерцание и начинает раздражать. Почему возникает мерцание и как его предотвратить? Хорошо, что вы задали этот вопрос. Объекты OLE, активизируемые на месте, не могут определить, правильно ли выглядит их текущее изображение, поэтому они всегда перерисовываются. При деактивизации объекта контейнер также не может судить о правильности воспроизведения объекта и на всякий случай перерисовывает его. Пользуясь OCX 96, элемент может узнать, должен ли он перерисовать себя при активизации, и сообщить контейнеру, нужно ли контейнеру перерисовать элемент при деактивизации. Это делается через новый интерфейс узла IOleInPlaceSiteEx, производный от IOleInPlaceSite. Помимо методов IOleInPlaceSite, данный интерфейс содержит три новых метода: OnInPlaceActivateEx, OnInPlaceDeactivateEx и RequestUIActivate. Первый из них используется для активизации объекта. Он получает указатель на флаг, которому контейнер присваивает значение TRUE, если элемент должен быть перерисован после завершения активизации, и FALSE в противном случае. Контейнер отвечает за присвоение этому флагу правильного значения, для чего ему приходится заниматься относительно сложным изучением недействительности изображения на отдельных участках объекта, выяснять z-порядок (то есть порядок наложения объектов от дальних к ближним) и т. д. Если во время активизации этот параметр равен NULL или же он активизируется старым методом IOleInPlaceSite::InInPlaceActivate, то элемент должен вернуться к старому варианту поведения и всегда перерисовывать себя. Второй параметр этого метода используется внеоконными элементами. Второй метод, OnInPlaceDeactivateEx, служит для деактивизации элемента. Он получает параметр логического типа, который сообщает контейнеру, должен (FALSE) или не должен (TRUE) элемент перерисовываться при деактивизации.
www.books-shop.com
Последний метод интерфейса, RequestUIActivate, должен вызываться элементом перед его UI-активизацией. Если контейнер не разрешает активизацию элемента, он возвращает S_FALSE — в этом случае элемент должен прекратить активизацию и вызвать OnUIDeactivate.
3.43 Графический вывод без мерцания Для единственного объекта, который твердо убежден в отсутствии объектов позади и впереди него, рисование не представляет никаких проблем — он просто рисует, не задумываясь ни о чем. Тем не менее когда имеешь дело с внеоконными элементами, которые могут быть прозрачными, за элементами или перед ними вполне могут находиться другие объекты, усложняющие процесс рисования. Впрочем, с ним можно успешно справиться, если элементы будут рисоваться в z-порядке, начиная с самых дальних. К сожалению, это может вызвать мерцание экрана, оно раздражает пользователей и производит неприятное впечатление. Традиционное решение этой проблемы в Windows — нарисовать все необходимое на внеэкранном растре и затем скопировать его в окно — быстро и никакого мерцания. Тем не менее в зависимости от размера растра и количества цветов в нем такой вариант может потребовать больших расходов памяти, и потому он не всегда идеален. Существует и другой механизм, который на текущий момент считается наиболее сложным — рисовать, начиная с ближних объектов и заканчивая дальними. Для предохранения нарисованных фрагментов используются области отсечения. Тем не менее форма некоторых объектов — например, текста или непрямоугольных объектов — делает процесс отсечения чрезвычайно сложным и выводит его на грань невозможного. Возможно, в будущем эволюция Windows облегчит этот процесс (а может, и не облегчит). Итак, что же делать? Спецификация OCX 96 не дает единственно верного решения, но предоставляет механизм, при помощи которого контейнер может реализовать любое сочетание трех алгоритмов рисования. Благодаря этому контейнер может выбрать любую степень простоты или сложности графического вывода. Например, при помощи новых аспектов, которые могут поддерживаться элементами OCX 96, контейнер может организовать рисование в два прохода. Первый проход рисует объекты от ближних к дальним, и от каждого объекта требуется нарисовать лишь непрозрачные аспекты, которые могут быть легко и просто отсечены для второго прохода (следовательно, непрозрачные аспекты обычно состоят из одной или нескольких прямоугольных областей). При втором проходе элементы рисуются в порядке от дальних к ближним, и каждый объект должен нарисовать свои прозрачные аспекты. Элементы могут сообщить контейнеру, должен ли он вызвать их во время второго прохода для рисования прозрачных частей. Описанный двухпроходный алгоритм не полностью избавляет от мерцания, однако во многих ситуациях он позволяет добиться разумного компромисса. Если контейнер реализует рисование при помощи внеэкранного растра, первый проход пропускается, а каждый объект должен полностью перерисовать себя в порядке от дальних к ближним. Спецификация OCX 96 вводит два новых аспекта: один (DVASPECT_OPAQUE) предназначен для непрозрачных и быстро отсекаемых участков, а другой (DVASPECT_TRANSPARENT) — для прозрачных участков и участков сложной формы. Элементы не обязаны поддерживать какой-либо из этих аспектов (хотя они всегда должны поддерживать аспект DVASPECT_CONTENT, относящийся ко всему объекту), а контейнер может определить, какие аспекты он должен поддерживать, при помощи метода IViewObjectEx::GetViewStatus. Пользуясь этим методом, контейнер может решить, какие аспекты того или иного объекта должны рисоваться и в какой последовательности, поэтому он может организовать рисование так, как
www.books-shop.com
считает нужным — он вовсе не обязан рисовать в два прохода лишь потому, что элемент поддерживает все аспекты. Однако если все же контейнер выберет двухпроходное рисование и на первом проходе воспользуется аспектом DVASPECT_OPAQUE, то второй проход он должен выполнять с DVASPECT_TRANSPARENT. OLE требует, чтобы все объекты считались прямоугольными независимо от их настоящей формы, поэтому непрямоугольные объекты должны по запросу возвращать габаритные прямоугольники, внутри которых они полностью помещаются. Начало координат любого аспекта всегда должно находиться в левом верхнем углу его габаритного прямоугольника. Метод IViewObject2::GetExtent теперь распознает новые аспекты и возвращает один и тот же габаритный прямоугольник для всех аспектов. GetRect, другой новый метод интерфейса IViewObject, возвращает контейнеру прямоугольник, который соответствует запрошенному аспекту. Для аспекта DVASPECT_CONTENT он будет совпадать с размерами всего объекта. Для DVASPECT_OPAQUE результат зависит от того, можно ли представить все непрозрачные участки элемента в виде одного прямоугольника — то есть элемент должен гарантировать, что его непрозрачная область имеет прямоугольную форму. В этом случае метод возвращает соответствующий прямоугольник. В противном случае возвращаемый HRESULT должен содержать DV_E_DVASPECT. Контейнер использует полученный прямоугольник для того, чтобы отсечь непрозрачную область элемента во время второго прохода при двухпроходному алгоритму рисования. Если запрошен аспект DVASPECT_TRANSPARENT, то возвращаемый прямоугольник должен накрывать всю прозрачную область объекта (это означает, что для удобства программирования он может накрывать и часть непрозрачных областей). По этому прямоугольнику контейнер определяет, перекрывают ли другие объекты прозрачные области объекта. Контейнер получает информацию о поддерживаемых элементом аспектах, вызывая метод IViewObjectEx::GetViewStatus, который также возвращает информацию о том, является ли объект полностью непрозрачным и имеет ли он сплошной фон. Обычно поддерживаемые элементом аспекты остаются постоянными на протяжении его жизненного цикла, однако прозрачность и тип фона могут довольно часто меняться. Элемент должен сообщить своему контейнеру об изменении информации, возвращаемой методом GetViewStatus, при помощи расширения интерфейса IAdviseSink, которое называется IAdviseSinkEx. В этом расширении имеется дополнительный метод OnViewStatusChange, который должен вызываться элементом при появлении изменений. Чтобы определить, поддерживает ли контейнер IAdviseSinkEx, элемент может, как обычно, вызвать QueryInterface через интерфейсный указатель, передаваемый ему при вызове IViewObject::SetAdvise. Если интерфейс не поддерживается, элементу приходится возвращаться к работе через IAdviseSink. Для обеспечения правильной прорисовки старых объектов считается, что все элементы, не поддерживающие IViewObjectEx, являются прозрачными.
3.44 Проверка попадания Проверкой попадания называется процедура определения объекта (а нередко и конкретной части объекта), на которой был сделан щелчок мышью. С обычными оконными элементами проверка попадания выполняется довольно просто, поскольку окно соответствующего элемента получает сообщение мыши. Не возникает особых сложностей и с полностью непрозрачными, прямоугольными внеоконными объектами, поскольку нужный объект может быть определен контейнером. Однако для прозрачных объектов и/или объектов непрямоугольной формы (которые сейчас по определению являются внеоконными) проверка попадания заметно усложняется. Для того чтобы облегчить проверку попадания при подобных обстоятельствах, OCX 96 использует интерфейс IViewObjectEx. После того как контейнер определит, что заданная точка лежит в пределах габаритного
www.books-shop.com
прямоугольника элемента (помните, что этот прямоугольник должен заведомо включать все точки объекта, так что непрямоугольный объект будет заполнять его лишь отчасти), он может вызвать метод IViewObject::QueryHitPoint, чтобы дать элементу возможность сказать «да, это мое» или «нет, я тут ни при чем». На самом деле элемент обладает несколько большей свободой выбора. Он может указать, что произошло попадание, то есть щелчок мышью пришелся на непрозрачную область элемента, или же что попадание было близким, то есть щелчок пришелся настолько близко к непрозрачной области, что его можно рассматривать как возможное попадание. Кроме того, он может сообщить, что точка попадания является прозрачной — это позволяет контейнеру определить, какой элемент находится под данным, и передать проверку попадания ему. Наконец, он может сообщить, что попадание произошло за его пределами — это означает, что щелчок мышью был наверняка сделан не на данном элементе. Случай «близкого попадания» оказывается достаточно интересным. Элемент получает возможность самостоятельно определить, что он вкладывает в это понятие, хотя контейнер передает ему «подсказку» при вызове метода. Значение подсказки представляет собой предположение контейнера о том, какое попадание следует считать «близким» в единицах HIMETRIC, однако элемент может выбрать свою собственную интерпретацию. Любой элемент, реализующий IViewObjectEx, обязан поддерживать DVASPECT_CONTENT для QueryHitPoint и на свое усмотрение может поддерживать и другие аспекты. Если метод вызывается для неподдерживаемого аспекта, элемент должен вернуть значение E_FAIL, которое сообщает контейнеру о необходимости запросить DVASPECT_CONTENT. IViewObjectEx также поддерживает другой метод, QueryHitRect, который сообщает контейнеру, пересекается ли заданный прямоугольник (то есть соприкасается ли хотя бы в одной точке) с габаритным прямоугольником данного аспекта. И снова обязательной является лишь поддержка DVASPECT_CONTENT, все прочие аспекты относятся к необязательным, а возвращаемое значение E_FAIL сообщает контейнеру о необходимости пользоваться DVASPECT_CONTENT. В отличие от QueryHitPoint метод QueryHitRect может ответить лишь «да» или «нет».
3.45 Прочие изменения и добавления в OCX 96 Помимо всего, что говорилось выше о внеоконных и неактивных элементах, спецификация OCX 96 наделила элементы некоторыми другими интересными возможностями. В этом коротком разделе они будут рассмотрены более подробно.
3.45.1 Быстрая активизация Первое из этих дополнений — «быстрая активизация», которая уменьшает время загрузки элемента и, следовательно, повышает его скорость работы. Ее работа обеспечивается несколькими общими интерфейсами, необходимыми для элемента и для его контейнера, и значениями нескольких свойств окружения. Быстрая активизация предшествует подготовке элемента к работе контейнером посредством IPersistxxx:Load или IPersistxxx::InitNew. Контейнер, поддерживающий быструю активизацию, запрашивает у элемента новый интерфейс IQuickActivate. Если элемент реализует этот интерфейс, контейнер заполняет структуру QACONTAINER указателями на ключевые интерфейсы, необходимые элементу (IOleClientSite, IAdviseSink, IPropertyNotifySink и принадлежащую контейнеру реализацию интерфейса событий элемента), и значениями некоторых свойств окружения. Логические свойства окружения объединяются в структуре QACONTAINER в одно значение типа DWORD, тогда как для остальных в структуре присутствуют специальные поля (например, для хранения свойства ForeColor в структуре предусмотрено поле типа OLE_COLOR с именем colorFore). Если контейнер не
www.books-shop.com
может передать указатели на некоторые из своих интерфейсов, он должен присвоить соответствующим полям структуры QACONTAINER значение NULL, а элемент позднее при необходимости должен вызвать для них QueryInterface. Аналогично, если некоторые из свойств окружения не реализуются контейнером, он должен передать для них разумные стандартные значения. Элемент получает структуру, работает с нужными полями (в частности, подключается к интерфейсам-приемникам, предоставляемым контейнером) и затем возвращает контейнеру в структуре QACONTROL свой набор сведений. В нем можно найти указатель на интерфейс элемента IViewObjectEx и манипуляторы (cookies) для подключенных точек соединения. Интерфейс IQuickActivate также содержит два других метода, SetContentExtent и GetContentExtent, при помощи которых контейнер может задать и получить габаритные размеры элемента.
3.45.2 Отмена OCX 96 описывает перспективную и достаточно сложную стратегию отмены (undo) и возврата (redo) действий для элементов. Такая стратегия необходима, поскольку построенное из компонентов приложение требует, чтобы каждый компонент участвовал в стандартной схеме отмены/возврата, управляемой контейнером, — в противном случае отмена и возврат просто не будут работать. Отмена на самом деле вполне заслуживает отдельной главы, но я не могу себе позволить такую роскошь! Конечно, эта тема важна, но мы не будем рассматривать ее в этой книге. Вместо этого вы сможете найти все отталкивающие подробности в спецификации (на компакт-диске, прилагаемом к книге).
3.45.3 Изменение размеров элементов Прямые манипуляции с элементами могут выполняться не только программистом на стадии разработки, но и в некоторых случаях это может происходить во время работы приложения. Все это замечательно, но иногда в ходе таких манипуляций элемент может стать слишком маленьким (или слишком большим). Ему нужны средства для того, чтобы запретить подобное обращение с собой или же сказать контейнеру: «Послушай, вот идеальный размер, который я бы хотел иметь, — что ты об этом думаешь?» Для подобных вещей в OCX 96 используется интерфейс IViewObjectEx, в котором имеется метод GetNaturalExtent. Он получает длинный список параметров, включая перечисляемый тип всего для двух возможных значений — DVEXTENT_CONTENT и DVEXTENT_INTEGRAL. Первое из них позволяет элементу указать, какие размеры он бы хотел иметь для точного отображения его содержимого; при помощи второго элемент исправляет размер, предложенный ему контейнером. Другие параметры этого метода содержат дополнительную информацию для элемента — например, воспроизводимый аспект (например, вывод на печать или отображение в виде значка) и контекст устройства, в котором должен быть отображен элемент после того, как завершится обсуждение его размеров.
3.45.4 Преобразование координат событий Если вернуться к разделу «Координаты» этой главы, можно заметить, что контейнеру иногда приходится осуществлять преобразования между системой координат, используемой элементами, и той, что используется самим контейнером. Кроме того, для свойств и методов стали своего рода стандартом координаты, измеряемые в пунктах («пункт» равен 1/72 дюйма). Тем не менее параметры событий обычно представляются в единицах
www.books-shop.com
HIMETRIC. OCX 96 ликвидирует эту непоследовательность и переводит параметры событий в пункты, снимая тем самым с контейнера бремя преобразования. Когда контейнер, не поддерживающий OCX 96, управляет элементом, написанным в соответствии с этой спецификацией, он получает значения координатных параметров событий в пунктах. Тем не менее тип таких параметров, указанный в библиотеке типов, сообщит ему об этом, так что проблемы с совместимостью не возникают. С другой стороны, когда контейнер, поддерживающий OCX 96, управляет старым элементом, значения координат в параметрах событий будут представлены не в пунктах — в этом случае контейнеру следует осуществить преобразование под руководством элемента (как это было раньше!), когда элемент вызывает IOleControlSite::TranslateCoordinates до передачи параметров.
3.45.5 Стандартные dispid Спецификация OCX 96 добавляет несколько стандартных идентификаторов диспетчеризации для распространенных свойств и методов. Они описаны в следующей таблице.
Dispid
Описание
Используется для MousePointer или другого свойства с аналогичным именем; спецификация OCX 96 определяет ряд значений, ссылающихся DISPID_MOUSEPOINTER (на стандартные курсоры мыши, а специальное 521) значение 99 говорит об использовании пользовательского курсора (cм. следующее свойство). Используется для MouseIcon или другого свойства с аналогичным именем, если значение DISPID_MOUSEICON (-522) свойства, представленного посредством DISPID_ MOUSEPOINTER (см. выше), равно 99. DISPID_PICTURE (-523) DISPID_VALID (-524)
Используется для свойства Picture. Используется для свойства, которое показывает, содержит ли элемент допустимые данные. Данное свойство имеет логический тип.
Новое свойство окружения, которое позволяет DISPID_AMBIENT_PALETTE элементу получить принадлежащий контейнеру (-726) текущий логический номер палитры Windows.
3.45.6 Связывание данных Спецификация OCX 96 слегка усложняет схему связывания данных. Например, представьте себе, что у вас имеется связанный элемент — скажем, текстовое поле. В большинстве случаев это поле должно осуществить действия, сопряженные со связыванием данных, только после того, как пользователь закончит редактировать содержимое поля — то есть когда оно потеряет фокус. Тем не менее некоторые элементы могут захотеть немедленно отражать в базе данных все внесенные изменения, даже если фокус продолжает оставаться у элемента. Примером может послужить кнопка-переключатель или флажок, от состояния которых зависит содержимое остальных элементов формы. Для свойств таких элементов в библиотеке типов может быть указан новый атрибут ImmediateBind.
3.46 Изменения в элементах ActiveX В документе Microsoft «OLE Controls/COM Objects for the Internet» (который входит в состав ActiveX SDK и присутствует на прилагаемом к книге компакт-
www.books-shop.com
диске) перечислены все добавления и изменения в спецификации элементов, сопровождающие переход от элементов OLE к элементам ActiveX. Тем не менее каждый элемент OLE является элементом ActiveX, даже если он появился задолго до выхода спецификации Элементов ActiveX. Другими словами, этот документ описывает только новые и изменившиеся положения, которые относятся (или оказываются наиболее полезными) при работе с Internet. Не надейтесь найти в нем перечень отличий между элементами OLE и ActiveX. Взгляните на ситуацию иначе — элементы OLE умерли, да здравствуют элементы ActiveX! В этом документе описывается пара новых стандартных dispid для свойства, при помощи которого можно узнать, готов ли элемент к работе (или же он продолжает загружать себя или свои данные), и событие, возбуждаемое элементом при изменении состояния готовности. Кроме того, в нем можно найти несколько новых интерфейсов и компонентных категорий. Первое требование, которое предъявляется к элементу ActiveX, который хочет хорошо работать в условиях Internet, — чтобы он поддерживал как можно больше интерфейсов семейства IPersistxxx. Это позволяет контейнеру добиться максимальной гибкости. В особенности желательно поддерживать интерфейс IPersistPropertyBag, поскольку это позволяет контейнеру оптимизировать процесс сохранения элемента в текстовом формате — это особенно важно при сохранении свойств элемента в HTML-потоке на webстранице. Элементы, поддерживающие только один вариант IPersist (за исключением IPersistPropertyBag), должны пометить себя как обязательных пользователей соответствующей компонентной категории. Если элемент обладает большими свойствами (например, растрами или видеоклипами), он почти наверняка постарается реализовать их как путевые (см. раздел предыдущей главы, посвященный асинхронным моникерам). Это позволяет сделать элемент более подходящим для работы с Internet, поскольку значения свойств могут загружаться асинхронно от элемента — при этом страница появляется на экране и подготавливается к работе гораздо быстрее, чем в случае обычных свойств. Элемент, поддерживающий путевые свойства, также обязан сделать следующее:
Поддержать IOleObject или IObjectVisible (новый интерфейс — см. ниже). Пометить в библиотеке типов путевые свойства как связываемые и пометить их как путевые при помощи соответствующих нестандартных атрибутов (также см. ниже). Пометить в библиотеке типов вспомогательный класс нестандартным атрибутом для путевых свойств (см. ниже). Выполнять правила создания моникеров и обеспечения устойчивости, пользуясь интерфейсом IBindHost (в случае, если он доступен). Уметь связываться с асинхронными моникерами и получать данные через IBindStatusCallback. Обеспечить приоритет инициализации и получения по сравнению с прорисовкой и как можно быстрее начать взаимодействие с пользователем. Поддержать свойство ReadyState и возбуждать события OnReadyStateChange в нужные моменты. Поддержать интерфейс IPersistPropertyBag, чтобы обеспечить оптимальную текстовую передачу HTML-атрибутов PARAM.
В нескольких ближайших главах мы увидим, каким образом элементы ActiveX описываются в HTML-страницах. Эта глава в первую очередь посвящена спецификациям, поэтому давайте рассмотрим некоторые положения, включенные в список. Путевые свойства — это всего лишь свойства, значения которых хранятся отдельно от остальных устойчивых данных элемента; их местонахождение задается через «путь данных». Путь данных обычно представляется в виде URL (вспомним, что URL прекрасно может описывать самые обычные
www.books-shop.com
файлы — нужно лишь использовать префикс file://), с ним можно связаться, чтобы получить фактические значения свойств. Связывание может быть как синхронным, так и асинхронным, хотя с точки зрения пользователя асинхронный вариант явно предпочтительнее, так как он приносит сплошные преимущества без единого недостатка. Чтобы получить данные для путевого свойства, элемент требует у контейнера преобразовать строку URL путевого свойства в URL-моникер и затем осуществляет связывание с этим моникером при помощи метода BindToStorage. В этот момент элемент выбирает, должно ли связывание и пересылка данных осуществляться асинхронно. Путевые свойства могут иметь любой тип, хотя из-за того, что хранимые в них объекты в конечном счете преобразуются в URL, они обычно имеют тип BSTR. Чтобы пометить свойство как путевое, следует указать для него в библиотеке типов элемента нестандартный атрибут GUID_PathProperty. Фактический тип данных свойства указывает при помощи одного или нескольких значений типа MIME. Путевые свойства помечаются как связываемые, поскольку принадлежащие контейнеру средства просмотра свойств могут работать независимо от страниц свойств, поэтому для их синхронизации приходится пользоваться интерфейсом IPropertyNotifySink. Вспомогательный класс также должен иметь нестандартный атрибут GUID_ HasPathProperties, основная задача которого — заявить о наличии у класса одного или нескольких путевых свойств. Значение атрибута равно количеству путевых свойств у элемента.
Нестандартные атрибуты в библиотеках типов Нестандартные атрибуты являются относительно новым (лето 1996 года) расширением библиотек типов, и для их чтения используется новый интерфейс ITypeInfo2. Они предоставляют удобную возможность для произвольного расширения библиотек типов. С каждым нестандартным атрибутом связан определенный GUID и необязательный набор параметров, поэтому на ODL/IDL свойство с нестандартным атрибутом может выглядеть следующим образом:
[ propget, custom(GUID_MyCustomAttribute, "Adam") ] BSTR SpecialProperty(int nIndex); Новое свойство окружения, которое позволяет элементу получить принадлежащий контейнеру текущий логический номер палитры Windows. У нас имеется свойство SpecialProperty с двумя ODL-атрибутами — propget и нестандартным. Нестандартный атрибут определяется GUID GUID_MyCustom Attribute и имеет один строковый параметр со значением «Adam». Для преобразования значения путевого свойства в моникер элемент должен иметь доступ к интерфейсам контейнера, обычно такое преобразование выполняется через IBindHost. Элемент может получить доступ к своему узлу, поскольку большинство элементов реализует интерфейс IOleObject, а в нем имеется метод SetClientSite, который вызывается контейнером. Тем не менее элементы не обязаны реализовывать интерфейс IOleObject, если им не нужна его функциональность. По этой причине был определен новый, упрощенный интерфейс IObjectWithSite, содержащий два метода — один для получения указателя на узел, а другой для его задания. Если элемент обладает путевыми свойствами, он должен поддерживать либо IOleObject, либо IObjectWithSite. После того как элемент получит указатель на интерфейс узла, он может через него запросить интерфейс IBindHost, однако лишь косвенно, поскольку
IBindHost не обязан входить в объект узла — это часть контейнера. Следовательно, элемент должен сначала вызвать через полученный указатель метод QueryInterface для интерфейса IServiceProvider, после чего он сможет вызвать IServiceProvider::QueryService для того, чтобы получить указатель на IBindHost. Интерфейсы IServiceProvider и QueryService аналогичны IUnknown и QueryInterface, за исключением того, что QueryInterface должен возвращать интерфейсные указатели для того же объекта (хотя это правило часто нарушается), тогда как от QueryService это не требуется (хотя и не запрещается). Разумеется, работа с элементами в условиях Internet имеет и другие нюансы, однако глава 13 целиком посвящена именно этой теме. Познакомив вас с основами, я позднее покажу, как реализовать полученные знания на практике. Мое углубленное рассмотрение OLE и COM, а также их роли для элементов ActiveX, подходит к концу. В следующей главе описываются инструменты Microsoft для создания элементов ActiveX, в числе которых C++ (с использованием шаблона Win32 BaseCtl, ATL и MFC), Visual Basic и Java.
Глава
4
Программные инструменты Microsoft для создания элементов ActiveX Если вы прочитали предыдущие главы, то сейчас вы знаете все о программах-компонентах, о роли COM и OLE и об основных принципах их работы. Кроме того, вы понимаете специфику COM и OLE и их связь с элементами ActiveX. В этой главе мы постараемся применить полученные знания на практике, чтобы вы смогли заняться созданием настоящих элементов ActiveX.
www.books-shop.com
Я начну с усовершенствования программы AutoProg и добавлю в нее несколько новых интерфейсов точек соединения, используемых элементами. В итоге у нас получится рабочий элемент, обладающий несколькими методами и одним событием (следует помнить, что «элементом» называется любой объект с интерфейсом IUnknown. Наш элемент раскрывает несколько интерфейсов, помимо IUnknown, чтобы приносить хоть какую-то пользу). Элемент будет реализован в виде выполняемого файла, а не в виде DLL-библиотеки. При желании вы можете пропустить этот фрагмент, хотя знакомство с ним поможет понять последующие разделы, в которых я буду переписывать наш элемент, пользуясь другими программными средствами. Вы убедитесь в том, что написать интерфейс точки соединения не так уж сложно, а использование специальных программных средств приводит к дальнейшему упрощению.
4.1 Реализация новых интерфейсов Эта глава целиком посвящена созданию элементов ActiveX при помощи средств, предоставляемых языками Microsoft. Если вы осилили главы 2 и 3, то сейчас у вас может возникнуть безумное желание — научиться создавать элементы ActiveX без всяких вспомогательных средств. Наша книга не пытается полностью раскрыть эту тему, однако в этом разделе я покажу, как реализовать некоторые COMинтерфейсы, используемые элементами. Есливы пишете свой элемент на C++ и пользуетесь простейшими средствами автоматизации (например, ATL), то вам придется работать на достаточно низком уровне и досконально понимать работу всех интерфейсов, используемых элементом. А теперь давайте займемся добавлением интерфейсов точек соединения в существующую программу.
ЗАМЕЧАНИЕ Повторяю — вы можете не знать ничего из сказанного и все равно создавать прекрасные элементы ActiveX.
Начнем с программы AutoProg, поскольку она представляет собой базовый сервер Automation. С AutoProg можно поэкспериментировать и добавить точку соединения для событий, наделить ее поддержкой визуального редактирования или внеоконного рисования — вы увидите, что сделать это не так уж сложно. Для начала добавим в AutoProg поддержку точек соединения. Что касается всего остального, то я не стану возиться с программированием и представлю вам программные средства, которые сильно упрощают эту задачу. Для экспериментов я возьму версию программы AutoPro2, завершенную к концу 3 главы. Я собираюсь наделить ее поддержкой точек соединения «для лентяев» — такое выражение обусловлено следующими ограничениями:
Интерфейс IEnumConnectionPoints не реализован. Интерфейс IEnumConnections не реализован. Поддерживается всего одна точка соединения, притом для очень простого интерфейса событий. Точки соединения представляют собой отдельные COM-объекты, наподобие клиентских узлов, но в нашем варианте реализация IConnectionPoint принадлежит тому же большому объекту, что и оставшаяся часть программы.
Весь исходный текст приложения находится в каталоге \CODE\CHAP04\AUTOPRO3 на сопроводительном диске CD-ROM. Для поддержки точек соединения необходимо следующее:
4.2 Реализация интерфейсов IProvideClassInfo и IProvideClassInfo2.
Реализация интерфейса IConnectionPointContainer. Реализация интерфейса IСonnectionPoint. Интерфейс событий. Описание интерфейса событий в библиотеке типов.
www.books-shop.com
В листинге 4-1 приведена изменившаяся часть файла AUTOPROG.H, цветом выделены определения новых классов вместе с изменениями в существующих классах AutoProg. Обратите внимание на два опережающих объявления классов CAutoDisp и CAutoCPC в начале файла. Они необходимы потому, что указатели на объекты этого класса встречаются в других местах до того, как будут определены сами классы. Без этих опережающих объявлений компилятор пожалуется, что ему неизвестно, на что ссылаются эти указатели. Первым определяется класс CAutoPCI2, реализующий интерфейс IProvideClassInfo2 и производный от него (он также реализует IProvideClassInfo, являющийся базовым для IProvideClassInfo2). Несколько начальных строк определяют методы интерфейса, которые могут вызываться извне средствами COM. Затем следует конструктор класса, которому передается указатель на класс реализации двойственного интерфейса CAutoDisp. Этот указатель нужен для того, чтобы при необходимости класс CAutoPCI2 мог обратиться к интерфейсу Automation (как мы вскоре увидим, при помощи этого указателя он поручает обработку своих методов IUnknown интерфейсу Automation). Указатель хранится в закрытой переменной m_pAutoDisp. Затем следует класс CAutoCP, реализующий интерфейс IConnectionPoint. И снова начальные строки относятся к методам интерфейса. За ними следует конструктор, получающий указатель на объект CAutoCPC (о нем будет рассказано ниже). Конструктор сохраняет указатель в переменной m_pCPC и присваивает m_dwCookie значение 0. Переменная m_dwCookie используется для хранения манипулятора («волшебного числа»), который идентифицирует связь, установленную через точку соединения. Другая переменная, m_pEvents, содержит указатель на интерфейс, к которому подключена точка соединения (то есть интерфейс-приемник). Последняя функция, GetEventIP, возвращает m_pEvents, если имеется установленное соединение, и NULL при его отсутствии. Возвращаемое значение преобразуется в указатель на IDispatch, поскольку точка соединения знает, что она будет общаться с реализацией интерфейса диспетчеризации. Третий класс, CAutoCPC, является производным от реализуемого им интерфейса IConnectionPointContainer. Этот класс устроен почти так же, как и два предшествующих, но при этом он содержит переменную m_AutoCP типа CAutoCP — то есть объект, представляющий точку соединения. При конструировании внедренного объекта ему передается указатель на внешний объект CAutoCPC; для этого в конструкторе CAutoCPC и присутствует m_AutoCP(this). Класс CAutoCPC также получает в своем конструкторе указатель на объект CAutoDisp и сохраняет его в m_pAutoDisp. Как и объект точки соединения, CAutoCPC содержит дополнительную функцию (она тоже называется GetEventIP), которая просто возвращает значение, полученное от функции GetEventIP объекта точки соединения. За этим классом следует слегка видоизмененное определение класса CAutoDisp, который реализует двойственный интерфейс, раскрываемый объектом. Этот класс теперь содержит два внутренних объекта, CAutoPCI2 (для IProvideClassInfo2) и CAutoCPC (для IConnectionPointContainer), в переменных m_PCI2 и m_CPC соответственно. Во время конструирования этим объектам передается указатель this объекта CAutoDisp. Кроме того, появилась новая функция FireYourEvent, используемая для возбуждения события через точку соединения. Листинг 4-1.AUTOPROG.H — определения новых классов для интерфейсов точки соединения, а также необходимые изменения в существующих классах
CAutoDisp() : m_PCI2(this), m_CPC(this) { m_ulRefs = 0; m_lSalary = 0; } private: void FireYourEvent(void); ULONG m_ulRefs; long m_lSalary; CAutoPCI2 m_PCI2; CAutoCPC m_CPC; }; Теперь перейдем к реализации интерфейсов. Первый интерфейс, IProvideClassInfo2, ставит перед нами первую задачу. Он содержит всего два метода, не считая методов IUnknown: GetClassInfo и GetGIUD. Первый метод получает указатель на реализацию ITypeInfo, которая сообщает информацию типа для объекта вспомогательного класса, представляемого приложением. Означает ли это, что мы должны также реализовать ITypeInfo? К счастью, нет, но это означает, что нам придется воспользоваться библиотекой типов. Вспомните версии этой программы из главы 2 — библиотека типов регистрируется в системе, после чего указатель на интерфейс ITypeInfo для двойственного интерфейса сохраняется в главном объекте приложения. Теперь нам понадобился интерфейс ITypeInfo и для вспомогательного класса. Можно было бы изменить CAutoProg::RegisterTypeLibrary, чтобы функция извлекала и сохраняла оба указателя, но я не хочу модифицировать существующий код без крайней необходимости. Вместо этого мы пойдем в обратном направлении и воспользуемся методом GetContainingTypeLib интерфейса ITypeInfo для получения указателя на ITypeLib для библиотеки типов в целом. Затем мы вызываем метод GetTypeInfoOfGuid этого интерфейса, чтобы получить информацию типа вспомогательного класса (CLSID_CAuto) и сохранить его во втором параметре функции. Подобный трюк выглядит не слишком элегантно, но зато работает. Второму методу, GetGUID, передается флаг, указывающий, какой именно GUID необходимо получить. В настоящее время этот флаг может иметь единственное значение GUIDKIND_DEFAULT_SOURCE_DISP_IID. Маловероятно, чтобы в ближайшем будущем были определены другие значения. Этот флаг сообщает функции о необходимости вернуть IID интерфейса диспетчеризации для источника по умолчанию, который в элементе превратится в интерфейс событий. Мы еще не успели определить этот новый интерфейс (его очередь наступит чуть позже в этом разделе), однако известно, что его IID равен IID_EAuto (имя было выбрано мною произвольно — возможно, оно не соответствует стандартам имен COM). Эта функция делает не так уж много — она присваивает IID переданному параметру и возвращает S_OK. Я поставил интерфейс IProvideClassInfo2 на первое место в основном для того, чтобы можно было узнать требования интерфейса точки соединения. С учетом этого следует определить интерфейс событий и добавить к нему информацию из библиотеки типов. У нас будет всего одно событие:
void OurEvent(void) Изменения в ODL-файле AutoPro3 приведены в листинге 4-2. Первым из них является определение dispinterface EAuto (dispinterface используется IDispatch — он не является самостоятельным интерфейсом). Обратите внимание на то, что dispinterface имеет собственный UUID, равный IID_EAuto. У него нет ни одного свойства и есть один метод OurEvent, не получающий параметров и не возвращающий никакого значения. Dispid этого метода равен 1. Вспомогательный класс был изменен, чтобы EAuto входил в него как интерфейс диспетчеризации источника по умолчанию. Листинг 4-2. AUTOPROG.ODL
www.books-shop.com
// Интерфейс событий для AutoProg [ uuid(BFA36560-AD71-11CF-ABBC-00AA00BBB4DA), helpstring("Autoprog server events interface") ] dispinterface EAuto { properties: methods: [ id(1) ] void OurEvent(); }; //
{ *pGUID = IID_EAuto; return S_OK; } return E_INVALIDARG; } Методы AddRef, Release и QueryInterface попросту поручают свою работу соответствующим методам реализации CAutoDisp. Это означает, что счетчик ссылок CAutoDisp уже не ограничивается подсчетом ссылок только на себя. Тем не менее поскольку все интерфейсы реализуются как внутренние классы CAutoDisp или CAutoCPC, один из них не может существовать без остальных, поэтому подобный подсчет ссылок в данном случае вполне допустим. Функцию CAutoDisp::QueryInterface нужно будет изменить, чтобы она умела распознавать IProvideClassInfo и IProvideClassInfo2, но я сделаю это позже, поскольку в нее придется добавить еще несколько интерфейсов. Итак, с этим интерфейсом все было просто. Интерфейс IConnectionPointContainer, приведенный в листинге 4-4, также не вызывает особых проблем. Его методы IUnknown выглядят аналогично приведенным выше, так что я не стал включать их в листинг. Я не стал реализовывать интерфейс-итератор для точек соединения в контейнере, поэтому метод EnumConnectionPoints возвращает стандартное значение E_NOTIMPL, которое сообщает о том, что метод не реализован. На самом деле написать EnumConnectionPoints не так уж сложно, поскольку он похож на любой другой стандартный COM-итератор (тексты которых можно найти в книге Крейга Брокшмидта «Inside OLE», Microsoft Press, 1995). Листинг 4-4. Реализация интерфейса IConnectionPointContainer
STDMETHODIMP CAutoCPC::EnumConnectionPoints(LPENUMCONNECTIONPOINTS *) { return E_NOTIMPL; } STDMETHODIMP CAutoCPC::FindConnectionPoint(REFIID iid, LPCONNECTIONPOINT *ppCP) { if (IsEqualGUID(iid, IID_EAuto)) { return m_AutoCP.QueryInterface(IID_IConnectionPoint, (void **)ppCP); } return CONNECT_E_NOCONNECTION; } FindConnectionPoint выглядит просто, потому что я поддерживаю всего одно соединение — с интерфейсом событий. Если передаваемый этому методу IID совпадает с IID интерфейса событий, я возвращаю указатель на точку соединения, вызывая метод QueryInterface внутреннего объекта CAutoCP (реализующего IConnectionPoint) для IID IConnectionPoint. Обратите внимание на то, что здесь нет никакого мошенничества — каждая реализация IConnectionPoint должна представлять собой отдельный объект, поэтому объект, реализующий IConnectionPointContainer, не должен быть тем же объектом, который реализует IConnectionPoint. Вот почему я пользуюсь принадлежащей CAutoCPC переменной типа CAutoCP (m_AutoCP) для того, чтобы добраться до метода QueryInterface интерфейса IConnectionPoint. Остается лишь рассмотреть реализацию интерфейса IConnectionPoint, приведенную в листинге 45. Листинг содержит все методы, включая методы интерфейса IUnknown, поскольку здесь они выглядят несколько иначе. Листинг 4-5. Реализация интерфейса IConnectionPoint из файла AUTOPROG.CPP
Поскольку объект точки соединения не совпадает с объектом ее контейнера, он обладает самостоятельной реализацией метода QueryInterface, которая распознает только IID_IConnectionPoint и IID_IUnknown. Впрочем, это не мешает ему пользоваться готовыми реализациями методов AddRef и Release, для чего он обращается через указатель к объектуконтейнеру m_pCPC, который, в свою очередь, вызывает AddRef и Release, реализованные CAutoDisp. Поскольку объект CAutoCP находится внутри объекта CAutoCPC, их жизненные циклы совпадают, а, значит, подобный подсчет ссылок будет вполне корректным. Метод GetConnectionInterface тривиален, я просто присваиваю переданному параметру IID интерфейса событий. Несложно написать и метод GetConnectionPointContainer — через сохраненный указатель на CAutoCPC я вызываю метод QueryInterface и запрашиваю у него IID_IConnectionPointContainer. С методом Advise дело обстоит несколько сложнее. В моей программе поддерживается всего одно соединение, поэтому я могу судить о том, было ли оно установлено ранее, проверяя переменную m_dwCookie. Если значение этой переменной отлично от 0, соединение уже установлено, поэтому я возвращаю стандартный HRESULT CONNECT_E_AdviseLimit. С другой стороны, если точка соединения еще не подключена, я вызываю QueryInterface через переданный интерфейсный указатель и запрашиваю у него указатель на его реализацию интерфейса событий. Если вызов завершается неудачей, возвращается другой стандартный HRESULT — CONNECT_E_CANNOTCONNECT. Если вызов QueryInterface окажется успешным, я присваиваю m_dwCookie значение 1 и возвращаю его. Кроме того, я сохраняю полученный от QueryInterface указатель в переменной m_pEvents типа LPUNKNOWN, позднее он будет использован для инициирования события. Как нетрудно догадаться, метод Unadvise решает противоположную задачу. Сначала он убеждается в наличии соединения и в том, что передаваемый манипулятор соединения совпадает с ранее использованным (вызывающая сторона не знает, или точнее — не должна знать, что в нашем классе этот манипулятор может быть равен только 0 или 1!). Если манипуляторы не совпадают, возвращается CONNECT_E_NOCONNECTION. В противном случае я освобождаю интерфейсный указатель из m_pEvents и присваиваю m_dwCookie значение 0. Наконец, мы подошли к последнему методу EnumConnections. Так как он не реализован, я возвращаю E_NOTIMPL. Остается лишь воспользоваться интерфейсом событий, подключенным к объекту. Предположим, событие должно инициироваться, если обновленное значение Salary окажется кратным 100. Соответствующий метод Payraise приведен в листинге 4-6. Листинг 4-6. Измененный метод Payraise
STDMETHODIMP CAutoDisp::Payraise(long lSalaryIncrement) { m_lSalary += lSalaryIncrement; if ((m_lSalary % 100) == 0) { FireYourEvent(); } return NO_ERROR; } Теперь необходимо написать функцию CAutoDisp::FireYourEvent. Я вызываю GetEventIP, функцию для доступа к точке соединения, чтобы получить указатель на интерфейс диспетчеризации. Если его значение отлично от 0 (то есть имеется установленное соединение), через полученный указатель вызывается Invoke. И снова я постарался до предела упростить вызов Invoke — вопервых, вызываемый метод не имеет ни аргументов, ни кода возврата, а во-вторых, не проверяется ни возвращаемое Invoke значение, ни структура исключения. Предполагается, что вызов Invoke всегда заканчивается успешно. Кстати говоря, первый параметр Invoke представляет собой dispid функции, вызываемой «с другой стороны». Поскольку я сам определил интерфейс, мне не требуется запрашивать значение этого dispid — он равен 1. В листинге 4-7 приведен полный текст функции FireYourEvent. Листинг 4-7. Функция FireYourEvent
void CAutoDisp::FireYourEvent()
www.books-shop.com
{ LPDISPATCH lpEvents; if (lpEvents = m_CPC.GetEventIP()) { EXCEPINFO ex; unsigned int uTmp; DISPPARAMS dp = { 0, 0, 0, 0 }; lpEvents -> Invoke(1, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dp, 0, &ex, &uTmp); } } Настоящая задача этой программы — продемонстрировать, что точки соединения (и механизм работы с событиями в частности) не являются привилегией элементов, любой COM-объект может создавать и возбуждать события. Вряд ли AutoProg3 можно назвать настоящим элементом ActiveX, скорее это COM-объект с точкой соединения для событий. Как проверить эту программу и убедиться, что она действительно работает? Если вы знакомы с HTML или обладаете соответствующими программными инструментами, попробуйте внедрить ее в Microsoft Internet Explorer, кроме того, можно использовать ее в Visual Basic 5.0. На случай, если вам захочется опробовать ее на C++, я поместил в каталог \CODE\CHAP04\TEST очень простую тестовую программу (и присвоил ей оригинальное имя test). Тем не менее если вы захотите протестировать программу, не забудьте предварительно запустить ее (чтобы зарегистрировать библиотеку типов) и затем внести в реестр данные REG-файла, находящегося в том же каталоге. Программа test — вполне обычное MFC-приложение, созданное при помощи AppWizard. В ней имеется меню Test с несколькими командами. Первая, Create, пытается создать экземпляр объекта посредством вызова CoCreateInstance, которому в качестве параметра передается IID интерфейса объекта IProvideClassInfo2. Если объект будет успешно создан, команда Create блокируется, а команды Delete, GetClassInfo, GetGUID и Connect в меню Test становятся доступными. Команда Delete удаляет объект из памяти, вызывая Release для указателя, полученного от CoCreateInstance. Команды GetClassInfo и GetGUID вызывают соответствующие методы IProvideClassInfo2, однако результат можно увидеть лишь при пошаговом выполнении программы в отладчике (да, вы совершенно правы — я написал эту программу, чтобы лишний раз убедиться в правильности работы AutoProg!). Команда Connect подключает точку соединения объекта к реализации EAuto внутри тестового приложения. После того как связь будет успешно установлена, команда Connect блокируется, а команды Disconnect и Try It становятся доступными. Disconnect разрывает связь, установленную командой Connect. Try It вызывает метод Payraise объекта AutoProg со значением 100, что должно привести к возбуждению события через точку соединения. В моей реализации событие должно просто выводить на экран окно сообщения. Как ни удивительно, именно это и происходит. Пожалуйста, учтите, что тестовое приложение было написано исключительно для демонстрации изложенного материала — его возможности весьма ограничены, и оно не проверяет возможных ошибок там, где это следовало бы делать. Если вы подключились к точке соединения объекта AutoProg, необходимо выполнить команду Disconnect перед командой Delete; если этого не сделать, программа поведет себя непредсказуемо.
4.3 Упрощенные способы создания элементов Если вы прочитали раздел «Реализация новых интерфейсов» (в начале этой главы), то, возможно, у вас возник вопрос — зачем мы тратили время и усилия на то, что не приносит никакой практической пользы? Я всего лишь хотел продемонстрировать, что реализация самых необходимых интерфейсов порой не вызывает особых сложностей, однако возможности элементов не ограничиваются одними точками соединения и интерфейсами событий. Давайте попробуем написать не учебный, а настоящий элемент. Впрочем, мне бы не хотелось снова преодолевать все сложности, поэтому я пойду по более простому (и разумному) пути: воспользуюсь программными инструментами, предназначенными для разработки элементов. Когда в начале 1994 года средства для разработки элементов OLE впервые вышли за пределы Microsoft, существовал всего один вариант: OLE CDK (Control Developer’s Kit, пакет разработчика элементов), дополнение к 16-разрядному Microsoft Visual C++ 1.5 и 32-разрядному Visual C++ 2.0. Работа CDK в значительной степени опиралась на MFC — библиотеку классов,
поставлявшуюся вместе с Visual C++. После пары обновлений пакета с выходом очередных версий Visual C++, CDK был включен в состав Visual C++ 4.0 и MFC 4.0. С того времени поддержка разработки элементов с использованием MFC была значительно усовершенствована, сейчас она удовлетворяет многим требованиям спецификаций OCX 96 и ActiveX. Начиная с MFC 4.0, MFC-приложения могут легко управлять работой элементов OLE (хотя поддержка элементов OCX 96 пока отсутствует). Хотя общая стратегия, ориентированная на использование OLE CDK или MFC при разработке элементов OLE на C++, была удачной (если вы знаете MFC и умеете программировать на C++, создание элементов становится достаточно простым занятием), она обладает некоторыми недостатками:
Необходимо уметь программировать на C++. Необходимо знать MFC. Каждый элемент должен пользоваться двумя вспомогательными DLL-библиотеками (runtime-компонентами MFC и C). Применяется библиотека MFC, которой не хотят пользоваться некоторые программисты на C++. Реализация ряда нетривиальных возможностей (например, двойственных интерфейсов) оказалась усложненной. Хотя созданные при помощи MFC элементы невелики, программирование на «чистом» C++ позволяет уменьшить их требования к объему рабочей среды. Кроме того, отказ от MFC увеличивает скорость работы элементов, что становится особенно заметным во время загрузки и рисования.
Перечисленные выше доводы заставили Microsoft и другие фирмы заняться поиском альтернативных средств для создания элементов на C++ и других языках. На сегодняшний день Microsoft предлагает следующие варианты:
«Чистый» C++. C++ и библиотека ActiveX Template Library (ATL). C++ и шаблон ActiveX BaseCtl (включенный в ActiveX SDK в качестве примера). C++ и MFC. Visual Basic. Java.
Инструменты других фирм открывают перед вами дополнительные возможности. Большая часть этой книги посвящена разработке элементов на C++ при помощи разнообразных инструментов, хотя в этой главе я рассмотрю создание элементов на Java и Visual Basic.
4.4 Инструменты для создания элементов на C++ Для начала рассмотрим все варианты, связанные с программированием на C++ (за исключением «чистого» C++).
4.5 Создание элементов при помощи MFC Visual C++ и MFC включают все необходимое для разработки 32-разрядных элементов ActiveX на Intel-платформах. Установка Visual C++ и MFC предоставляет разработчику элементов следующие возможности:
Runtime-библиотеки MFC (в том числе и runtime-библиотеки C). Поддержка создания элементов на уровне MFC. Приложение «тестовый контейнер» для проверки элементов. Генератор основы элемента AppWizard. Обширная справочная система. Примеры, демонстрирующие разнообразные возможности элементов ActiveX.
Разработка элемента при помощи MFC начинается с вызова программы-мастера (wizard), предназначенной для создания элементов. Выполните команду File|New и выберите из списка типов создаваемых файлов строку Project Workspace; открывается окно диалога New Project Workspace, в котором среди прочих типов приложений присутствует и мастер элементов OLE (OLE ControlWizard). В том же диалоговом окне можно выбрать каталог, в котором будет находиться элемент, и платформы, для которых он должен быть скомпилирован. Для примера, описанного в
www.books-shop.com
нашей главе, выберите нужный каталог и введите имя проекта First. Нажатие кнопки Create запускает мастера элементов OLE, после чего на экране появляется следующее окно (рис. 4-1), в котором следует указать:
Количество элементов в DLL. Хотите ли вы ограничить применение элемента runtime-лицензией. Хотите ли вы включить комментарии в исходный текст. Хотите ли вы сгенерировать справочные файлы.
Для наших целей можно оставить без изменений все значения, принятые по умолчанию. Нажмите кнопку Next, чтобы перейти к следующему окну. В нем указывается:
Рис. 4-1. Первое диалоговое окно OLE ControlWizard
Имя элемента, которому принадлежат свойства, заданные в этом и последующих окнах диалога (вы можете отредактировать это имя, а также имена всех сгенерированных классов и файлов, для этого следует нажать кнопку Edit Names). Хотите ли вы, чтобы элемент активизировался при отображении. Хотите ли вы, чтобы элемент был невидимым в режиме выполнения. Хотите ли вы, чтобы он присутствовал в стандартном диалоговом окне Insert Object (то есть должен ли соответствующий элемент реестра содержать ключ Insertable или эквивалентную ему компонентную категорию). Должен ли мастер сгенерировать метод About и связанное с ним диалоговое окно. Хотите ли вы, чтобы элемент мог использоваться как «простая рамка» (simple frame) для объединения других элементов. Стандартный оконный класс Windows (если он имеется), подклассом которого должен являться создаваемый элемент.
4.5.1 И снова оставьте значения, принятые по умолчанию. Один проект, созданный OLE ControlWizard, может содержать несколько элементов, объединенных в одном OCX-файле. Второе окно мастера позволяет задать поведение каждого элемента на основании имени класса MFC, генерируемого для него мастером. Большинство элементов должно активизироваться при отображении, если контейнер поддерживает такое поведение (что справедливо для большинства контейнеров). При установке флажка Activates When Visible мастер устанавливает бит состояния OLEMISC_ACTIVATEWHENVISIBLE в соответствующей категории реестра. Отказ от установки этого бита (если он возможен) улучшает производительность работы элемента.
www.books-shop.com
Хотя подавляющее большинство элементов ActiveX имеет визуальное представление в режиме выполнения, некоторые из них не обладают пользовательским интерфейсом. Тем не менее такие элементы должны отображаться в режиме конструирования, чтобы пользователь мог увидеть их и выполнить необходимые операции. Простейшим примером может послужить элементтаймер Visual Basic, который в режиме конструирования можно увидеть и разместить на форме. Тем не менее в режиме выполнения формы таймер не имеет визуального представления. Если вы хотите наделить подобным свойством создаваемый элемент, установите флажок Invisible At Runtime. Некоторые элементы могут присутствовать не только в специализированных диалоговых окнах, но и включаться в стандартное окно Insert Object. Для этого необходимо включить в соответствующий элемент реестра ключ Insertable (или эквивалентную ему компонентную категорию), как было сказано в главах 2 и 3. OLE ControlWizard содержит флажок Available In Insert Object Dialog, при установке которого ключ Insertable автоматически вставляется в нужный элемент реестра. Каждый пользователь мечтает узнать, кто и когда написал его любимый элемент. Впрочем, давайте честно признаемся: подобная информация в первую очередь тешит самолюбие авторов элемента. В спецификации Элементов ActiveX определен специальный dispid для метода AboutBox. Установка флажка Has An About Box автоматически включает в ваш элемент этот метод и задает для него простейшую реализацию. При необходимости можно легко модифицировать этот стандартный код и, конечно, расширить возможности самого метода. Если создаваемый элемент будет содержать другие элементы ActiveX (например, групповой элемент или трехмерная панель), вам понадобится поддержка протокола ISimpleFrameSite. Включить ее несложно: достаточно установить флажок Acts As A Simple Control во втором окне диалога (в главе 17 этот вопрос рассматривается более подробно). Некоторые элементы, которые несколько лет поставлялись вместе с Visual Basic, а также устаревшие нестандартные элементы, входившие в SDK, представляют собой модификации стандартных элементов Microsoft Windows, обладающие расширенными возможностями. Например, вам может понадобиться элемент-список, который бы обладал специальным поведением при выполнении операций drag-and-drop. OLE ControlWizard позволяет сделать так, чтобы создаваемый элемент являлся подклассом какого-либо стандартного элемента Windows и обладал всеми его возможностями, однако при этом вы бы имели возможность управлять нужными аспектами его поведения. Поле со списком (combo box) в нижней части второго диалогового окна мастера содержит имена оконных классов, для которых можно создавать подклассы. В Visual C++ 4.2 и более поздних версий во втором окне OLE ControlWizard имеется кнопка Advanced, при помощи которой задаются параметры, относящиеся только к элементам OCX 96 и ActiveX (в последующих версиях Visual C++ способ доступа к этим параметрам может измениться). При нажатии этой кнопки появляется диалоговое окно со следующими флажками:
Windowless Activation (внеоконная активизация). Unclipped Device Context (контекст устройства без отсечения). Flicker-Free Activation (активизация без мерцания). Mouse Pointer Notifications When Inactive (уведомления от курсора мыши в неактивном состоянии). Optimized Drawing Code (оптимизированный код графического вывода). Loads Properties Asynchronously (асинхронная загрузка свойств).
Если вы прочитали главу 3, то без труда сможете связать эти свойства с положениями спецификаций Элементов OCX 96 и ActiveX. Осталось лишь нажать кнопку Finish в окне мастера. Появляется текстовое окно с информацией, на которую после первого прочтения никто не обращает внимания, — смело нажимайте кнопку OK. Мастер создает файлы проекта и открывает его в Visual C++. В оставшейся части главы предполагается, что вы пользуетесь 32-разрядным Visual C++ 4.2 или более поздней версией под Windows 95 или Windows NT версии 4.0 и выше.
4.5.2 Так что же сделал мастер?
www.books-shop.com
После того как создание элемента завершится, давайте рассмотрим содержимое каждого из файлов, сгенерированных OLE ControlWizard. Make-файл FIRST.MAK включает как отладочную, так и окончательную версию элемента для кодировок ANSI и Unicode.
ЗАМЕЧАНИЕ Как объясняется в главе 3, возможности работы Unicode-элементов ограничиваются Windows NT, тогда как 32разрядные ANSI-элементы работают на всех платформах Win32. Unicode-версию следует строить лишь в том случае, если ваш элемент должен работать только под Windows NT или если вы строите все версии элемента, чтобы программа установки могла выбрать оптимальный вариант. Сгенерированный файл README.TXT содержит краткое описание всех созданных файлов. Среди прочих присутствует и DEF-файл проекта.
Файлы FIRST.RC и RESOURCE.H описывают ресурсы проекта; FIRST.RC включает RESOURCE.H, а последний используется и теми файлами на C++, которые должны работать с ресурсами. Значок элемента, отображаемый в диалоговом окне About по умолчанию, хранится в FIRST.ICO. Кроме того, элемент содержит растр, который изображает кнопку на панели инструментов при внедрении элемента в контейнеры типа Visual Basic. Этот растр хранится в FIRSTCTL.BMP. Если бы в проекте присутствовали и другие элементы, то для каждого из них был бы создан отдельный растровый файл, чтобы разные элементы по-разному изображались на панели инструментов. Как и в любой другой DLL-библиотеке, созданной при помощи MFC, каждый файл элемента содержит объект, производный от CWinApp. Если MFC применяется для создания элементов ActiveX, класс приложения обычно является производным от другого класса, COleControlModule, который, в свою очередь, является производным от CWinApp. Класс приложения, созданный мастером как производный от COleControlModule, называется CFirstApp и находится в файлах FIRST.CPP и FIRST.H. Класс самого элемента ActiveX является производным от CWnd (через промежуточный класс ColeControl). Класс элемента называется CFirstCtrl, он находится в файлах FIRSTCTL.CPP и FIRSTCTL.H. Класс страницы свойств CFirstPropPage является производным от COlePropertyPage и находится в файлах FIRSTPPG.CPP и FIRSTPPG.H. Класс COlePropertyPage является производным от CDialog. При помощи файлов STDAFX.CPP и STDAFX.H MFC организует эффективную работу с заранее компилированными заголовками. Остается лишь файл FIRST.ODL — автоматически сгенерированный ODL-файл для элемента (-ов) проекта. Только что сгенерированный файл практически пуст. В нем определяется сама библиотека типов с именем FIRSTLib, а также первичный интерфейс диспетчеризации элемента _DFirst (пока пустой) и первичный интерфейс событий _DFirstEvents (также изначально пустой). Наконец, в нем определяется вспомогательный класс First. По мере добавления свойств, событий и методов средствами пользовательского интерфейса происходит автоматическое обновление ODL-файла. После компиляции библиотека типов включается в состав ресурсов проекта. Включение библиотеки типов в ресурсы DLL-библиотеки объекта — официально рекомендованный способ хранения этой информации. Теперь давайте познакомимся с тремя классами, сгенерированными мастером, и изучим содержащийся в них код. Нам придется рассмотреть их достаточно подробно, чтобы понять, что именно было сгенерировано и как оно работает.
ЗАМЕЧАНИЕ Вы не обязаны знать все технические подробности, чтобы создавать элементы с помощью MFC. Тем не менее знание сгенерированного кода поможет вам разобраться в причинах неправильного поведения ваших элементов.
4.6 Класс модуля элемента: CFirstApp
www.books-shop.com
Если внимательно рассмотреть реализацию CFirstApp, можно заметить, что OLE ControlWizard создал код только для функций InitInstance и ExitInstance. Кроме того, он включил в этот же файл две глобальные (не принадлежащие конкретному классу) функции: DllRegisterServer и DllUnregisterServer. Эти функции экспортируются DLL-библиотекой, содержащей элемент, и используются для работы с реестром (занесения и удаления информации элемента). В листинге 4-8 приведено содержимое заголовочного файла FIRST.H, а в листинге 4-9 — основная реализация элемента First. Листинг 4-8. Заголовочный файл FIRST.H
// First.h : основной заголовочный файл для FIRST.DLL #if !defined( __AFXCTL_H__ ) #error include ‘afxctl.h’ before including this file #endif #include "resource.h" // Основные символические константы ///////////////////////////////////////////////////////////////// // CFirstApp : реализация содержится в First.cpp class CFirstApp : public COleControlModule { public: BOOL InitInstance(); int ExitInstance(); }; extern const GUID CDECL _tlid; extern const WORD _wVerMajor; extern const WORD _wVerMinor; Заголовочный файл включает RESOURCE.H, в котором содержатся определения для каждого идентификатора ресурса, использованного в программе. Затем он объявляет класс CFirstApp как производный от COleControlModule, обладающий лишь двумя открытыми функциями (при желании можно добавить к ним другие). Затем следуют объявления трех глобальных переменных (как внешних ссылок — хотя эти переменные определены в FIRST.CPP, они используются за его пределами, при этом в соответствующем месте включается заголовочный файл). Переменная _tlid содержит GUID библиотеки типов элемента, а переменные wVerMajor и _wVerMinor образуют номер версии элемента. В листинге 4-9 сначала создается глобальный экземпляр класса CFirstApp с именем theApp и определяются три глобальные переменные, первой из которых присваивается GUID библиотеки типов, а двум оставшимся — номер версии 1.0. Затем определяется функция CFirstApp::InitInstance, которая подготавливает модуль к работе, вызывая InitInstance базового класса, после чего в нее можно вставить пользовательский код. ExitInstance выглядит аналогично. Пользовательский код должен быть вставлен в процедуру выхода перед вызовом реализации ExitInstance базового класса. Далее следуют две глобальные функции регистрации: DllRegisterServer и DllUnregisterServer. DllRegisterServer вызывает макрос AFX_MANAGE_STATE, подготавливающий класс типа AFX_MAINTAIN_STATE, в котором содержится контекст модуля. После этого функция пытается зарегистрировать библиотеку типов элемента, для чего применяется более функциональная разновидность способа, использованного мной в AutoPro3. Если попытка заканчивается неудачно, функция возвращает код ошибки _E_TYPELIB. Если регистрация библиотеки прошла нормально, DllRegisterServer пытается зарегистрировать сам элемент в реестре. В случае неудачи возвращается код ошибки SELFREG_E_CLASS, в противном случае функция завершает свою работу нормальным образом. Функция DllUnregisterServer выглядит аналогично, за исключением того, что она отменяет регистрацию библиотеки типов и удаляет данные элемента из реестра.// First.cpp : реализация CFirstApp и регистрация DLLбиблиотеки
#include "stdafx.h" #include "First.h" #ifdef _DEBUG #define new DEBUG_NEW
www.books-shop.com
#undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif CFirstApp NEAR theApp; const GUID CDECL BASED_CODE _tlid = { 0xa29db7d2, 0xe4e5, 0x11cf, { 0x84, 0x8a, 0, 0xaa, 0, 0x57, 0x54, 0xfd } }; const WORD _wVerMajor = 1; const WORD _wVerMinor = 0; ////////////////////////////////////////////////////////////////// // CFirstApp::InitInstance — Инициализация DLL-библиотеки BOOL CFirstApp::InitInstance() { BOOL bInit = COleControlModule::InitInstance(); if (bInit) { // Добавьте свой код инициализации модуля } return bInit; } ////////////////////////////////////////////////////////////////// // CFirstApp::ExitInstance — Завершение работы DLL-библиотеки int CFirstApp::ExitInstance() { // Добавьте свой код завершения модуля return COleControlModule::ExitInstance(); } ////////////////////////////////////////////////////////////////// // DllRegisterServer — Внесение информации в реестр STDAPI DllRegisterServer(void) { AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE)) return ResultFromScode(SELFREG_E_CLASS); }
return NOERROR;
////////////////////////////////////////////////////////////////// // DllUnregisterServer — Удаление информации из реестра STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))
www.books-shop.com
return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE)) return ResultFromScode(SELFREG_E_CLASS); return NOERROR; }
4.6.1 Класс элемента: CFirstCtrl Для того чтобы наделить элемент необходимыми возможностями, вам придется изменять в первую очередь класс элемента CFirstCtrl. Это справедливо по отношению ко всем элементам ActiveX, созданным при помощи MFC: основная функциональность элемента заключена в классе, производном от COleControl. Содержимое заголовочного файла FIRSTCTL.H приведено в листинге 4-10. Листинг 4-10. Заголовочный файл FIRSTCTL.H
Фабрика класса и guid GetTypeInfo Идентификаторы страниц свойств Имя типа и информация состояния
// Схема сообщений //{{AFX_MSG(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять элементы функции. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_MSG DECLARE_MESSAGE_MAP() // Схема диспетчеризации
www.books-shop.com
//{{AFX_DISPATCH(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять функции. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DISPATCH DECLARE_DISPATCH_MAP() afx_msg void AboutBox(); // Схема событий //{{AFX_EVENT(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять функции. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_EVENT DECLARE_EVENT_MAP() // Идентификаторы диспетчеризации и событий public: enum { //{{AFX_DISP_ID(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // элементы перечисления. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DISP_ID }; }; Листинг начинается с определения класса. Макрос DECLARE_DYNCREATE подготавливает динамическое создание класса в соответствии с нормами MFC. В файле реализации присутствует парный макрос IMPLEMENT_DYNCREATE. За макросом следует конструктор, виртуальная функция рисования (OnDraw), функция устойчивости свойств (DoPropExchange) и OnResetState. Последняя функция вызывается, когда контейнер обращается к элементу с запросом сбросить свое состояние; при этом свойствам присваиваются значения по умолчанию. Затем объявляется деструктор, за которым следуют четыре подозрительных макроса. Они (как и некоторые другие макросы MFC) проделывают немалую работу. DECLARE_OLECREATE_EX объявляет функции для подготовки фабрики класса объекта, DECLARE_OLETYPELIB объявляет функции для получения указателя на интерфейс ITypeLib библиотеки типов элемента и для кэширования библиотеки типов (оптимизация, выполняемая MFC), DECLARE_PROPPAGEIDS объявляет функцию класса для получения CLSID страницы свойств элемента. Наконец, макрос DECLARE_OLECTLTYPE объявляет функции класса для получения ProgID элемента и значений различных битов состояния. Далее следуют пустые объявления схем сообщений и диспетчеризации, а также объявление функции, вызываемой при обращении к методу AboutBox. За ними следует пустое объявление схемы событий и пустое перечисление, в котором будут сохраняться dispid свойств, методов и событий по мере их добавления. Мы подошли к файлу реализации FIRSTCTL.CPP, приведенному в листинге 4-11. Листинг 4-11. Файл реализации FIRSTCTL.CPP
// FirstCtl.cpp : реализация класса элемента OLE CFirstCtrl #include #include #include #include
"stdafx.h" "First.h" "FirstCtl.h" "FirstPpg.h"
#ifdef _DEBUG
www.books-shop.com
#define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif IMPLEMENT_DYNCREATE(CFirstCtrl, COleControl) /////////////////////////////////////////////////////////////// // Схема сообщений BEGIN_MESSAGE_MAP(CFirstCtrl, COleControl) //{{AFX_MSG_MAP(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять макросы схемы сообщений. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_MSG_MAP ON_OLEVERB(AFX_IDS_VERB_PROPERTIES, OnProperties) END_MESSAGE_MAP() /////////////////////////////////////////////////////////////// // Схема диспетчеризации BEGIN_DISPATCH_MAP(CFirstCtrl, COleControl) //{{AFX_DISPATCH_MAP(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять макросы схемы диспетчеризации. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DISPATCH_MAP DISP_FUNCTION_ID(CFirstCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE) END_DISPATCH_MAP() /////////////////////////////////////////////////////////////// // Схема событий BEGIN_EVENT_MAP(CFirstCtrl, COleControl) //{{AFX_EVENT_MAP(CFirstCtrl) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять обработчики событий. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_EVENT_MAP END_EVENT_MAP() /////////////////////////////////////////////////////////////// // Страницы свойств // При необходимости добавьте дополнительные страницы свойств. // Не забудьте увеличить значение счетчика! BEGIN_PROPPAGEIDS(CFirstCtrl, 1) PROPPAGEID(CFirstPropPage::guid) END_PROPPAGEIDS(CFirstCtrl) /////////////////////////////////////////////////////////////// // Инициализировать фабрику класса и GUID
/////////////////////////////////////////////////////////////// // Информация типа для элемента static const DWORD BASED_CODE _dwFirstOleMisc = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE | OLEMISC_RECOMPOSEONRESIZE; IMPLEMENT_OLECTLTYPE(CFirstCtrl, IDS_FIRST, _dwFirstOleMisc) /////////////////////////////////////////////////////////////// // CFirstCtrl::CFirstCtrlFactory::UpdateRegistry // Добавляет или удаляет элементы реестра для CFirstCtrl BOOL CFirstCtrl::CFirstCtrlFactory::UpdateRegistry(BOOL bRegister) { // Убедитесь, что ваш элемент соответствует требованиям // совместной потоковой модели. Подробности приведены // в документе MFC TechNote 64. // Если элемент нарушает требования совместной модели, // необходимо модифицировать следующий фрагмент программы // и заменить 6-й параметр с afxRegApartmentThreading на 0.
///////////////////////////////////////////////////////////////// // CFirstCtrl::CFirstCtrl — конструктор CFirstCtrl::CFirstCtrl() { InitializeIIDs(&IID_DFirst, &IID_DFirstEvents); // Инициализируйте данные экземпляра вашего элемента } //////////////////////////////////////////////////////////////// // CFirstCtrl::~CFirstCtrl — Destructor CFirstCtrl::~CFirstCtrl() { // Очистите данные экземпляра вашего элемента } /////////////////////////////////////////////////////////////// // CFirstCtrl::OnDraw — функция рисования void CFirstCtrl::OnDraw( CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { // Замените следующий фрагмент вашим кодом рисования pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH))); pdc->Ellipse(rcBounds); } //////////////////////////////////////////////////////////////// // CFirstCtrl::DoPropExchange — поддержка устойчивости void CFirstCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); // Вызовите функции PX_ для каждого // устойчивого нестандартного свойства } /////////////////////////////////////////////////////////////// // CFirstCtrl::OnResetState — сброс элемента // в состояние по умолчанию void CFirstCtrl::OnResetState() { COleControl::OnResetState();
}
// Присваивает значения // по умолчанию //из DoPropExchange
// Сбросьте любые другие параметры состояния элемента
//////////////////////////////////////////////////////////////// // CFirstCtrl::AboutBox — отображение диалогового окна About
www.books-shop.com
void CFirstCtrl::AboutBox() { CDialog dlgAbout(IDD_ABOUTBOX_FIRST); dlgAbout.DoModal(); } //////////////////////////////////////////////////////////////// // Обработчики сообщений CFirstCtrl Первое, что заслуживает внимания в этом файле — пустая схема сообщений, в которой присутствует лишь ключ ON_OLEVERB, добавленный OLE ControlWizard. Он позволяет элементу реагировать на вызов команд (verbs) или стандартных действий OLE. Обычно контейнер заносит такую команду в меню Edit при выделении объекта. В настоящий момент для элемента определена всего одна команда Properties, которая заставляет его отобразить страницы свойств. Далее следует почти пустая схема диспетчеризации. Она состоит из одной строки для функции AboutBox, добавленной OLE ControlWizard при установке флажка About Box. За ней идет пустая схема событий и схема страниц свойств. Последняя содержит строку для каждой страницы свойств, используемой элементом, включая стандартные. Числовой параметр макроса BEGIN_PROPPAGEIDS представляет собой количество страниц свойств в схеме, а каждая строка содержит CLSID страницы. IMPLEMENT_OLECREATE_EX создает фабрику класса объекта и инициализирует ее переданными значениями CLSID и ProgID. Аналогично, IMPLEMENT_OLETYPELIB реализует функции, объявленные в DECLARE- версии этого же макроса в заголовочном файле. В двух следующих строках объявляются IID для первичных интерфейсов диспетчеризации и событий, а _dwFirstOleMisc инициализируется битами состояния OLEMISC элемента. Обратите внимание на флаги OLEMISC_CANTLINKINSIDE, который не позволяет использовать внедряемый объект как источник связывания, и OLEMISC_RECOMPOSEONRESIZE, который сообщает контейнеру, что объект хотел бы иметь возможность заново воспроизводить свое изображение при изменении размеров элемента внутри контейнера. Другие биты состояния уже рассматривались нами. За инициализацией _dwFirstOleMisc следует последний макрос «большой четверки», IMPLEMENT_OLECTLTYPE. Он создает функции для получения ProgID элемента и битов состояния. Функция CFirstCtrl::CfirstCtrlFactory::UpdateRegistry — один из полезных методов объекта, которые библиотека MFC реализует за вас. Он используется в программах для того, чтобы зарегистрировать элемент или удалить сведения о нем из системного реестра. Обратите внимание на комментарий, вставленный мастером, — по умолчанию все элементы создаются в соответствии с «совместной потоковой моделью» (см. врезку ниже), если только вы не нарушите это правило и не переориентируете свой элемент на однопоточную модель. Остается лишь реализация самого класса CFirstCtrl. Мастер ограничивается лишь минимумом функций, создаваемых в вашем производном классе. Позднее вы сможете добавить и другие функции, а также переопределить любые виртуальные функции базового класса. По умолчанию все действия конструктора сводятся к сохранению идентификаторов интерфейсов диспетчеризации и событий в переменных класса и блокировке его внутреннего кэширования библиотеки типов. Деструктор делает и того меньше — точнее, вообще ничего не делает. Следующая функция, OnDraw, выглядит поинтереснее. Она вызывается в тех случаях, когда элемент получает требование перерисовать себя. Эта функция, создаваемая OLE ControlWizard, делает нечто такое, что разработчик почти всегда немедленно удаляет из программы — она рисует эллипс! Для этого существуют две причины: во-первых, эллипс показывает, что элемент присутствует в контейнере, а во-вторых, он как бы продолжает серию примеров Circ из старого пакета Visual Basic Control Development Kit (CDK). Я не буду подробно рассматривать этот замечательный фрагмент и твердо обещаю заменить его в ближайшее время. Обратите внимание на параметры этой функции: в отличие от стандартных приложений MFC, она получает размеры прямоугольника, в котором может рисовать. Элементам ActiveX, как и любым другим объектам OLE, запрещается выводить что-либо за пределами этой области. Почему, спросите вы? Если элемент ActiveX отображается в окне контейнера, а не в собственном окне, то при нарушении границы передаваемого прямоугольника он может нарисовать что-нибудь поверх информации, принадлежащей контейнеру. Еще одно важное обстоятельство, которое следует учитывать при добавлении кода графического вывода в элемент ActiveX, заключается в следующем: не следует полагать, что левый верхний угол прямоугольника элемента имеет координаты (0, 0); если
www.books-shop.com
элемент находится в окне контейнера, то координаты (0, 0) будут соответствовать левому верхнему углу окна контейнера, а не элемента. Если установить какие-либо «специальные» флажки OCX 96 и ActiveX, вызываемые кнопкой Advanced, функция OnDraw может выглядеть несколько иначе, поскольку она может воспользоваться некоторыми средствами оптимизации графического вывода из спецификации OCX 96.
Потоковые модели COM Во всех версиях 16-разрядного и первом варианте 32-разрядного COM (в Windows NT 3.5) использование СОМ могло быть только однопоточным. Это означает, что только один поток данного приложения мог вызвать CoInitialize (или OleInitialize) и только в этом потоке могло происходить создание объектов и вызов методов интерфейсов. Если в 32-разрядном COM сразу несколько приложений пыталось одновременно обращаться к одному объекту, система ставила такие обращения в очередь, поэтому одному приложению приходилось ждать, пока будет обслужено другое. По вполне понятным причинам такая модель получила название «однопоточной» (single-threaded). Разумеется, ее использование облегчает жизнь разработчику объектов, поскольку ему не приходится беспокоиться о синхронизации данных в объекте. Тем не менее в многопоточных и многозадачных средах однопоточная модель заметно снижает производительность работы приложений. В Windows 95 и Windows NT 3.51 появилась новая, «совместная» потоковая модель (apartment model). В соответствии с ней, один поток приложения создает нужный объект и является единственным, из которого могут производиться обращения к объекту. Тем не менее другие потоки также могут обращаться к объекту, причем дело обходится без маршалинга интерфейсных указателей между потоками. Вызовы других потоков синхронизируются COM и передаются объекту через исходный, создавший его поток. Поскольку все обращения к экземпляру объекта осуществляются из одного потока, синхронизация по-прежнему не представляет проблем для объекта, если только он не обладает данными, которые должны совместно использоваться его различными экземплярами. Наконец, в Windows NT 4.0 была представлена «свободная потоковая модель», в соответствии с которой любой поток может обратиться к объекту. Разумеется, в этом случае разработчик объекта должен учесть возможность одновременного вызова методов интерфейса несколькими потоками. Более полное рассмотрение потоковых моделей COM, взятое из статьи для Microsoft Knowledge Base, приведено в приложении Б. Далее следует DoPropExchange. Данная функция используется для пересылки значений устойчивых свойств между переменными свойств и хранилищем, предоставленным контейнером. Пока она вызывает лишь функцию ExchangeVersion, которая сохраняет версию элемента, которая включается в устойчивое состояние элемента, и функцию DoPropExchange базового класса, осуществляющую фактическое сохранение всех стандартных свойств элемента, для которых это разрешено. Если такое поведение нежелательно, вы можете удалить вызов функции базового класса и самостоятельно организовать сохранение всех необходимых стандартных свойств. Функция OnResetState вызывается для того, чтобы «сбросить» свойства элемента, то есть вернуть им значения, принятые по умолчанию. Стандартная реализация, созданная мастером, вызывает соответствующую функцию базового класса. Если вам захочется выполнять какие-то другие действия, вставьте соответствующий код. Последняя функция класса, созданная мастером, вызывается при вызове метода AboutBox — она также называется AboutBox. Стандартная реализация создает экземпляр класса CDialog по шаблону диалогового окна, а затем вызывает для созданного объекта функцию DoModal, чтобы вывести на экран окно About. Чтобы создать более сложное окно About (например, сверхмодный вариант со скрытым списком команды разработчиков), необходимо заменить эту функцию.
4.6.2 Класс страницы свойств: CFirstPropPage Остается лишь рассмотреть класс страницы свойств. Как мы помним из главы 3, страница свойств представляет собой отдельную вкладку диалогового окна, открывающего доступ к некоторым свойствам элемента. Кроме того, страница свойств сама является COM-объектом. Элемент может занести на страницу свойств любую информацию (хотя обычно на ней содержатся пары имен и
www.books-shop.com
значений свойств) и использовать столько страниц, сколько считает нужным. OLE ControlWizard создает всего одну пустую страницу свойств. Он оформляет ее в виде класса MFC, производного от COlePropertyPage. В нашем случае страница называется CFirstPropPage. В листинге 4-12 приведен заголовочный файл FIRSTPPG.H, а в листинге 4-13 — файл реализации FIRSTPPG.CPP. Листинг 4-12. Заголовочный файл FIRSTPPG.H
// FirstPpg.h : объявление класса страницы свойств CFirstPropPage /////////////////////////////////////////////////////////////// // CFirstPropPage : реализация содержится в FirstPpg.cpp class CFirstPropPage : public COlePropertyPage { DECLARE_DYNCREATE(CFirstPropPage) DECLARE_OLECREATE_EX(CFirstPropPage) // Конструктор public: CFirstPropPage(); // Данные диалогового окна //{{AFX_DATA(CFirstPropPage) enum { IDD = IDD_PROPPAGE_FIRST }; // ВНИМАНИЕ — здесь ClassWizard будет добавлять данные. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DATA// Реализация protected: virtual void DoDataExchange(CDataExchange* pDX); // Поддержка DDX/DDV // Схемы сообщений protected: //{{AFX_MSG(CFirstPropPage) // ВНИМАНИЕ — здесь ClassWizard будет добавлять функции. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_MSG DECLARE_MESSAGE_MAP() }; Заголовочный файл FIRSTPPG.H не содержит ничего, кроме определения класса страницы свойств. Этот класс создается динамически и обладает фабрикой класса, как и класс элемента, но он не имеет библиотеки типов (то есть не управляется средствами Automation). Он имеет конструктор и схему данных, которая представляет собой область, куда Visual C++ заносит переменные и их объявления при закреплении переменных класса за объектом диалогового окна. В нашем случае схема данных временно пустует, в ней присутствует только перечисление с идентификатором ресурса страницы свойств (IDD_PROPPAGE_FIRST). Последняя оставшаяся функция DoDataExchange представляет собой стандартную процедуру обмена/проверки данных диалогового окна (DDX/DDV), при помощи которой происходит обмен данными между переменными класса и элементами диалогового окна. Объявление класса завершается областью для объявления функций, образующих схему сообщений. Содержимым этой области управляет Visual C++, и в данный момент, как и можно было ожидать, она пуста. Листинг 4-13. Файл реализации FIRSTPPG.CPP
#ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif IMPLEMENT_DYNCREATE(CFirstPropPage, COlePropertyPage) ////////////////////////////////////////////////////////////// // Схема сообщений BEGIN_MESSAGE_MAP(CFirstPropPage, COlePropertyPage) //{{AFX_MSG_MAP(CFirstPropPage) // ВНИМАНИЕ — здесь ClassWizard будет добавлять // и удалять элементы схемы сообщений. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_MSG_MAPEND_MESSAGE_MAP() /////////////////////////////////////////////////////////////// // Инициализировать фабрику класса и GUID IMPLEMENT_OLECREATE_EX(CFirstPropPage, "FIRST.FirstPropPage.1", 0xa29db7d5, 0xe4e5, 0x11cf, 0x84, 0x8a, 0, 0xaa, 0, 0x57, 0x54, 0xfd) /////////////////////////////////////////////////////////////// // CFirstPropPage::CFirstPropPageFactory::UpdateRegistry // Добавляет или удаляет элементы реестра для CFirstPropPage BOOL CFirstPropPage::CFirstPropPageFactory::UpdateRegistry( BOOL bRegister) { if (bRegister) return AfxOleRegisterPropertyPageClass( AfxGetInstanceHandle(), m_clsid, IDS_FIRST_PPG); else return AfxOleUnregisterClass(m_clsid, NULL); } ////////////////////////////////////////////////////////////// // CFirstPropPage::CFirstPropPage — конструктор CFirstPropPage::CFirstPropPage() : COlePropertyPage(IDD, IDS_FIRST_PPG_CAPTION) { //{{AFX_DATA_INIT(CFirstPropPage) // ВНИМАНИЕ — здесь ClassWizard будет инициализировать // переменные класса. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DATA_INIT} /////////////////////////////////////////////////////////////// // CFirstPropPage::DoDataExchange — осуществляет обмен данными // между страницей и свойствами void CFirstPropPage::DoDataExchange(CDataExchange* pDX) { //{{AFX_DATA_MAP(CFirstPropPage) // ВНИМАНИЕ — здесь ClassWizard добавит вызовы // функций DDP, DDV и DDV. // НЕ РЕДАКТИРУЙТЕ содержимое этого фрагмента // сгенерированного кода! //}}AFX_DATA_MAP DDP_PostProcessing(pDX); }
www.books-shop.com
/////////////////////////////////////////////////////////////// // Обработчики сообщений CFirstPropPage Класс CFirstPropPage не делает ничего сверхъестественного. Как и класс элемента ActiveX, он содержит пустую схему сообщений и вызов макроса IMPLEMENT_OLECREATE_EX с его CLSID и ProgID. Кроме того, он содержит объект вложенной фабрики класса и функцию UpdateRegistry, которая добавляет или удаляет сведения о странице свойств в системный реестр. Далее идет конструктор класса, который по умолчанию передает идентификатор диалогового окна и его заголовок конструктору базового класса и на этом завершает работу. Вы можете добавить в конструктор свой код, который будет выполнен перед отображением страницы. Осталась только функция DoDataExchange, которая по умолчанию вызывает функцию постобработки DDX DDP_PostProcessing. Поскольку страница свойств будет пополняться новыми переменными, Visual C++ будет автоматически добавлять в функцию код для переноса информации между новыми переменными и соответствующими им элементами диалогового окна.
4.7 Спецификации OCX 96 и ActiveX при создании элементов с использованием MFC Если установить в OLE ControlWizard любой из флажков, относящихся к элементам OCX 96 и ActiveX, сгенерированный код будет несколько отличаться от приведенного выше. Например, нередко приходится создавать элементы, которые не активизируются при отображении, используют внеоконную активизацию и поддерживают сообщения от мыши в неактивном состоянии. При установке соответствующих флажков в мастере исчезает флаг OLEMISC_ACTIVATEWHENVISIBLE, а в классе, производном от COleControl, появляется новая функция GetControlFlags. Ее исходный текст приведен в листинге 4-14. Листинг 4-14. Новая функция GetControlFlags, созданная для внеоконного элемента DWORD CNowndnoactivateCtrl::GetControlFlags()
{ DWORD dwFlags = COleControl::GetControlFlags(); // Элемент может активизироваться без создания окна. // При написании обработчиков сообщений элемента используйте // переменную m_hWnd лишь после предварительной проверки, // не равна ли она NULL. dw_Flags |= windowlessActivate;
}
// Элемент может получать сообщения от мыши // в неактивном состоянии. // Если вы пишете обработчики сообщений WM_SETCURSOR // и WM_MOUSEMOVE, используйте переменную m_hWnd // лишь после предварительной проверки, не равна ли она NULL. dw_Flags |= pointerInactive; return dwFlags;
Как нетрудно видеть из листинга, MFC скрывает от вас большинство сложностей, связанных с OCX 96. Написание элемента происходит почти так же, как и раньше, необходимо лишь помнить о том, что вам запрещено (например, использовать логический номер несуществующего окна). MFC определяет возможности элемента при помощи функции GetControlFlags и соответствующим образом изменяет его поведение. Аналогично, если вы захотите воспользоваться графической оптимизацией (контекст устройства без отсечения, активизация без мерцания и оптимизированный код графического вывода), основные отличия касаются флагов, возвращаемых GetControlFlags. Кроме того, изменяется функция OnDraw. Программа проверяет, поддерживает ли контейнер графическую оптимизацию, и если не поддерживает, вы обязаны освободить использованные объекты GDI и восстановить исходное состояние контекста устройства.
www.books-shop.com
Наконец, если установить флажок наличия свойств с асинхронной загрузкой, в классе элемента появляется новая переменная m_lReadyState, в которой содержится текущее состояние готовности элемента.
4.8 Runtime-библиотеки MFC Элементы, созданные при помощи MFC, постоянно пользуются этой библиотекой, следовательно, они должны быть большими и медленными, не так ли? Ничего подобного! Конечно, библиотека MFC достаточно велика, но работающие с ней элементы малы, поскольку они используют runtime-обращения к MFC DLL. Тем не менее если создаваемые вами элементы рассчитаны на работу в условиях Internet, желательно, чтобы пользователь имел копию этой DLL-библиотеки на своем компьютере, тогда ему останется лишь загрузить код вашего элемента (-ов). Разумеется, если у пользователя нет MFC DLL, ему придется загрузить и ее, после чего библиотека может использоваться всеми остальными элементами на базе MFC. Насколько быстро работает MFC? Чтобы окружающий мир принял элементы ActiveX так же легко, как он принял VBX, они просто не могут быть большими и медленными. На оптимизацию производительности и сокращение объема кода было потрачено немало усилий. По своему опыту могу сказать, что по скорости работы VBX практически не отличается от эквивалентного ему элемента ActiveX. Впрочем, по мере того, как мы будем создавать все более сложные элементы в оставшейся части этой книги, у вас сложится собственное мнение. Тем не менее во многих случаях элементы, построенные с использованием библиотек низкого уровня (например, ATL), работают быстрее тех, которые пользуются MFC. Вы должны решить, что вас интересует в первую очередь. Возможность простого создания элементов на C++? Тогда пользуйтесь MFC. С другой стороны, если вы хорошо представляете себе взаимодействие элементов с COM и при этом умеете работать с ATL, эти средства обычно позволяют ускорить работу элементов. Проблема размера элементов на базе MFC в значительной степени решается тем, что основная часть реализации классов COleControl, COlePropertyPage и COleControlModule, а также используемые этими классами возможности MFC находятся в специальной DLL-библиотеке. Помимо этих классов, она содержит и такие вещи, как стандартный шрифтовой и цветовой объекты. Эта DLL-библиотека существует в двух вариантах — окончательном и отладочном, а также в ANSI и Unicode-версии. Окончательная версия называется MFCxx.DLL, где xx заменяется номером версии MFC, к которой принадлежит DLL-библиотека. Отладочная версия имеет суффикс D — например, MFC40D.DLL. Unicode-версия имеет суффикс U — например, MFC40U.DLL или MFC40UD.DLL. Если OLE является системным компонентом Windows и поставляется вместе с операционной системой, runtime-библиотека MFC (пока) не относится к числу таких компонентов. Следовательно, вы должны позаботиться о том, чтобы ваша программа инсталляции устанавливала ее с обычной в таких случаях проверкой версии — необходимо проследить за тем, чтобы не стереть более новую версию.
4.9 Построение и тестирование элемента First в тестовом контейнере Все готово к построению элемента. Нажмите кнопку Build на панели инструментов и подождите некоторое время. Если проследить за текстом, проплывающим в окне вывода, вы заметите, что в конце построения элемент регистрируется в системном реестре. Чтобы удалить элемент из реестра, можно воспользоваться утилитой RegSvr32 (в каталоге \MSDEV\BIN). Чтобы удалить из реестра элемент (-ы), хранящийся в DLL, воспользуйтесь следующей командой:
regsvr32 /u control.ocx где control.ocx — настоящее имя DLL-библиотеки, содержащей нужный элемент (-ы). Для регистрации элементов используется следующая команда:
regsvr32 control.ocx
www.books-shop.com
ЗАМЕЧАНИЕ Удаление сведений из реестра может пригодиться при экспериментах с ActiveX. Объем информации в реестре может быстро перерасти все разумные пределы, так что удаление ненужной информации должно превратиться в полезную привычку. Так как же происходит тестирование элемента? Когда пакет OLE CDK впервые появился в сентябре 1994 года, элементы OLE могли использоваться всего в одном приложении (Microsoft Access версии 2.0), но даже этот продукт не обеспечивал полноты функций. Возникла необходимость в каком-нибудь средстве для тестирования элементов, вот почему Microsoft включила в OLE CDK и Visual C++ 4.x специальное приложение (тестовый контейнер). Хотя тестовый контейнер не имеет языка программирования и, следовательно, не позволяет написать сценарий для тестирования элемента, он предоставляет полный доступ к свойствам, методам, событиям и страницам свойств объекта, а также команды меню для тестирования элемента в определенных условиях. Тестовый контейнер представляет собой полезное средство предварительного тестирования.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Если у вас имеется более совершенный тестовый контейнер для элементов ActiveX (например, Microsoft Internet Explorer версии 3.0 и выше, Microsoft Visual Basic версии 4.0 и выше или же Microsoft Visual FoxPro версии 3.0 и выше), возможно, вам стоит тестировать элементы в этой среде. Я предпочитаю пользоваться тестовым контейнером для быстрого предварительного тестирования, а затем проводить более подробное тестирование на Visual Basic. Тестовый контейнер загружается гораздо быстрее Visual Basic, в этом состоит одна из причин, по которой я сначала пользуюсь тестовым контейнером.
4.10 Работа с тестовым контейнером Тестовый контейнер (TSTCON32.EXE) может запускаться непосредственно из меню Tools Visual C++. Разумеется, программу можно запустить и традиционным способом — например, из меню Start. После того как тестовый контейнер будет запущен, вставьте в него элемент ActiveX — для этого следует нажать первую кнопку на панели инструментов или выполнить команду Edit|Insert OLE Control. В любом случае открывается диалоговое окно Insert OLE Control, в котором приведен список всех элементов, зарегистрированных на вашем компьютере. Выберите из списка строку First Control, которая появляется после завершения построения элемента. На рис. 4-2 показано, как выглядит тестовый контейнер после вставки элемента.
Рис. 4-2. Элемент First в тестовом контейнере
www.books-shop.com
Элемент, вставленный в тестовый контейнер, остается выделенным, поэтому с ним можно выполнять необходимые операции. Для начала попробуем вызвать один из методов, которые содержатся в списке методов, поддерживаемых элементом. Выполните команду Edit|Invoke Methods; открывается диалоговое окно, изображенное на рис. 4-3.
Рис. 4-3.Диалоговое окно Invoke Control Method для элемента First В настоящий момент наш элемент поддерживает только метод AboutBox, выводящий диалоговое окно About для данного элемента. Попробуйте выполнить его, нажав кнопку Invoke. Если бы в элементе присутствовали и другие методы, они также присутствовали бы в списке диалогового окна Invoke Control Method. Вы могли бы выбрать метод, ввести любые необходимые параметры и выполнить его. Если метод возвращает какое-либо значение, оно будет выведено в нижней части диалогового окна. Тестовый контейнер получил информацию о методах элемента из его библиотеки типов. При этом он также узнает сведения о свойствах элемента. Тестовый контейнер строит для вас простой список свойств, что позволяет вам узнавать и задавать свойства элементов прямо из контейнера. Чтобы вывести соответствующее диалоговое окно, выделите элемент и выполните команду View|Properties. В нашем случае появившееся диалоговое окно не будет содержать ни одного свойства, которое можно было бы выбрать. Вы можете потребовать, чтобы элемент отобразил свой пользовательский интерфейс для чтения и задания свойств — страницу свойств. Для этого следует выделить элемент и выполнить команду Edit|Properties… First Control Object. Открывается пустая страница свойств, изображенная на рис. 4-4.
www.books-shop.com
Рис. 4-4. Страница свойств элемента First (пустая) Если ваш элемент обладает какими-либо событиями, вы можете их инициировать — выполните в тестовом контейнере команду View|Event Log или нажмите соответствующую кнопку панели инструментов. Если в элементе имеются связанные свойства, можно просмотреть сообщения от них командой View|Notification Log или соответствующей кнопкой панели инструментов. У тестового контейнера имеется одна интересная возможность — он позволяет задавать значения свойств окружения. Тестовый контейнер предоставляет всем узлам элементов одинаковый набор свойств окружения. Команда Edit|Set Ambient Properties выводит диалоговое окно Ambient Properties, изображенное на рис. 4-5, предназначенное для чтения и записи свойств окружения. Если ваш элемент обладает какими-либо возможностями или свойствами, которые могут наследовать свойства окружения, вы можете задать их значения в этом окне, вставить новый экземпляр элемента и посмотреть, как на нем отразились новые значения свойств окружения. В нескольких последующих главах мы увидим, как это делается. Последняя возможность, о которой стоит упомянуть, — перемещение и изменение размеров контейнера. Перемещение выполняется просто: установите курсор мыши над любым краем элемента, за исключением мест, в которых находятся маркеры размеров (они имеют вид черных квадратиков по углам и в середине каждой стороны), и перетащите элемент в новое положение. Перемещение не должно вызывать никаких действий со стороны элемента, он даже не получает запроса на перерисовку. Чтобы изменить размер элемента, захватите мышью один из маркеров и перетащите его в нужное положение. Поскольку мы работаем с функцией OnDraw, создаваемой по умолчанию, весь эффект от изменения размеров сводится к перерисовке большего или меньшего эллипса. Тестовый контейнер обладает рядом других возможностей, предназначенных для тестирования различных аспектов элемента. В их число входит сохранение и загрузка состояния элемента, сохранение и загрузка наборов свойств и проверка различных условий, связанных с активизацией на месте. Попробуйте поэкспериментировать с ними и посмотрите, для чего они предназначены (правда, при столь ограниченных возможностях элемента скорее всего они никак не проявят себя).
4.11 Создание элементов при помощи ActiveX Template Library (ATL) Незадолго до публикации первого издания этой книги Microsoft произвела изменения в команде разработчиков Visual C++. Новой команде предстояло определить, спроектировать и создать усовершенствованную версию продукта. Проект получил кодовое название Galileo (впрочем, к теме данной книги это не относится). Я стал менеджером проекта Galileo по программированию, отвечал за общую архитектуру и выбор библиотек классов, которые должны были поставляться вместе с продуктом. На ранней стадии работы мы поняли, что разработчики компаний стараются придерживаться новой стратегии. Они переходят на разработку многоуровневых систем, логика которых подразделялась следующим образом:
Пользовательский сервис — пользовательский интерфейс, переходы и логика выполнения программы. Деловой сервис — бизнес, деловые функции. Сервис данных — доступ к базам данных.
Разумеется, Microsoft хотела предоставить инструменты, которые бы помогали в разработке таких многоуровневых приложений. Предположив, что взаимодействие между уровнями будет организовано при помощи COM, мы пришли к выводу, что объекты уровня делового сервиса довольно часто будут представлять собой COM-объекты, которые должны обладать способностью параллельно обслуживать запросы от нескольких клиентов, и что основным требованием к ним будет скорость работы, даже за счет усложнения разработки. Инструменты Microsoft, предназначенные для разработки COM-объектов, в прошлом ориентировались именно на простоту программирования и не отвечали требованиям нового рынка. В основном они поддерживали однопоточную или, в лучшем случае, — совместную модель COM, что ограничивало реализацию многопользовательского сценария. По этим причинам команда Galileo решила создать небольшую библиотеку C++, которая бы могла удовлетворить эту потребность. После смены нескольких названий за библиотекой утвердилось название ActiveX Template Library, или ATL. В компании появилось множество желающих иметь ее код, а мы получили множество предложений по ее усовершенствованию, так что в итоге было решено сделать COM-компоненты ATL общедоступными. Так, в апреле 1996 года в World Wide Web появилась ATL 1.0. Летом 1996 года появилась ATL 1.1, в которой были исправлены некоторые ошибки, заметно доработана документация и примеры, а также поддерживались некоторые дополнительные возможности. Разумеется, мы продолжали получать новые предложения по доработке библиотеки. Постепенно ATL превратилась в основное средство Microsoft для разработки компактных элементов ActiveX на C++. Обратите внимание на отличия между ней и библиотекой MFC: последняя проектировалась для того, чтобы по возможности облегчить программисту на C++ процесс создания элементов и приложений, тогда как ATL преследовала совершенно иную
www.books-shop.com
цель — создавать на C++ элементы, отличающиеся малыми размерами и высокой скоростью. Не стоит полагать, будто ATL заменяет MFC. Для работы с ATL необходимо понимать суть шаблонов C++, а также знать COM значительно глубже, чем при использовании MFC. Если вы не очень точно представляете себе, что такое шаблон, внимательно прочитайте документацию по С++. В двух словах, «шаблон» напоминает что-то вроде грандиозного макроса — он позволяет определять функции и классы, работающие с обобщенным набором типов, причем при создании экземпляра шаблона создается специальный класс или функция для работы именно с этим типом. Например, следующий фрагмент
template< T > class MyClass { T *pNext; T *pPrev; }; определяет класс-шаблон с именем MyClass, содержащий две переменные — указатели на тип, задаваемый во время применения шаблона. Например, если создать класс для целых чисел:
MyClass< int > iMyClass; то созданный класс будет аналогичен следующему:
class iMyClass { int *pNext; int *pPrev; }; Шаблоны позволяют создавать классы и функции, которые надежно работают с конкретными типами и при этом не требуют создавать несколько версий одного класса в исходном тексте программы. Как следует из названия, библиотека ATL основана на применении шаблонов, в ней используется множественное наследование. Обобщенный COM-объект является производным от классашаблона CComObjectBase, поддерживающего интерфейсы IUnknown и IClassFactory (кроме того, по вашему выбору он может поддерживать объединение). Для поддержки двойственного интерфейса ваш класс должен также быть производным от класса-шаблона CComDualImpl, реализующего компоненты IDispatch двойственного интерфейса, и ISupportErrorInfo. ATL 1.x напрямую поддерживает многие полезные возможности элементов — в их число входят двойственные интерфейсы, точки соединения и итераторы. ATL 1.1 также поддерживает интерфейсы IClassFactory2 и IProvideClassInfo2. Тем не менее для создания полноценного элемента на базе ATL 1.x вам придется самостоятельно написать достаточно большой объем кода. На момент написания книги команда ATL упорно трудилась над определением набора возможностей библиотеки ATL 2.0, основная цель которой — облегчить создание элементов. Особое внимание должно быть уделено элементам OCX 96 и ActiveX. Тем не менее сейчас мне трудно привести более конкретную информацию, поскольку многие технические детали находятся в рабочем состоянии. Пока можно уверенно сказать, что в Visual C++ будет включена поддержка разработки элементов ActiveX при помощи ATL (мастер, средства IDE), которая заметно облегчит процесс их создания. Одна из важнейших задач, которые мы пытаемся решить, — чтобы вам не приходилось нести издержки, связанные с той или иной возможностью, если вы не хотите использовать/поддерживать эту возможность в своем элементе. Другими словами, если вы не пользуетесь страницами свойств, вам не придется отягощать свой элемент ненужным грузом, связанным со страницами. Этот же принцип распространяется и на runtimeбиблиотеки — их попросту нет. ATL включается в проект как исходный текст программы, и никакое связывание при этом не требуется. Кроме того, подобный подход позволяет вам установить параметры компилятора и компоновщика по своему усмотрению и не обрекать себя на параметры, с которыми были построены библиотеки. Если вы не хотите пользоваться RTTI (runtime-информация типа), не включайте их в свой проект. Построенные на базе ATL объекты живут самостоятельно и обычно загружаются значительно быстрее тех, чья работа зависит от runtime-библиотек. ATL обладает и другими преимуществами, которые могут заинтересовать разработчика элементов. Во-первых, история развития типичных библиотек классов показывает, что с каждой новой версией они обрастают новыми возможностями и в результате увеличиваются в размерах.
www.books-shop.com
В ATL мы постараемся как можно скорее остановить эту тенденцию к росту. После выпуска ATL 2.0 мы тщательно пересмотрим весь проект. Дальнейшие изменения будут вноситься в ATL в том (и только в том!) случае, если они действительно важны. Во-вторых, ATL отличается от привычных иерархических библиотек классов. Если в таких библиотеках, как MFC, разработка приложений облегчается за счет наследования, ATL предоставляет в ваше распоряжение набор вспомогательных классов-шаблонов, на базе которых будут строиться ваши классы. Это означает, что в ATL отсутствует эквивалент класса CWinApp, в ней нет документов и видов. Конечно, одна из причин заключается в том, что библиотека ATL предназначена исключительно для создания COM-объектов. Подробности, касающиеся ATL 2.0 и той поддержки, которая будет обеспечена средой Visual C++, все еще находятся в состоянии разработки. Можно быть уверенным в следующем:n Все предыдущие усилия по освоению ATL (например, написание программ при помощи ATL 1.x) окажутся полезными при создании элементов с ATL 2.x.
Мастер ATL, ориентированный на разработку элементов, позволит выбрать поддерживаемые интерфейсы и логически сгруппирует их по назначению.
Некоторые интерфейсы (например, IPerPropertyBag) могут использоваться элементами, созданными на базе ATL, однако ни ATL, ни среда Visual C++ не предоставляют прямой поддержки для них. Это означает, что вам придется написать свой собственный код. Если учесть, что методы этих интерфейсов все равно почти полностью состоят из нестандартного кода, вы почти ничего не теряете.
4.12 Создание элементов при помощи шаблона ActiveX BaseCtl Во время оптимизации процесса создания элементов на C++ после выхода OLE CDK команда Visual Basic и особенно Марк Ваншнидер (Marc Wanschnider) работала над новым шаблоном, который по сравнению с MFC находился значительно ближе к функциям API и интерфейсам, используемым элементами, и потому позволял добиться заметного роста производительности за счет усложнения использования и необходимости больших познаний в области COM. Тем не менее иногда преимуществ оказывалось достаточно для того, чтобы переписать некоторые существующие элементы. В начале 1996 года шаблон был включен в состав Microsoft Developer’s Network (MSDN), чтобы им могли пользоваться широкие массы. Шаблон распространялся исключительно в качестве примера, он не сопровождался никакой поддержкой со стороны фирмы, так что работать с ним можно было только самостоятельно. После первого выхода ActiveX SDK в начале 1996 года библиотека ATL находилась на стадии проектирования первой версии (без явной поддержки элементов), поэтому разработанный командой Visual Basic шаблон был включен в SDK под названием ActiveX BaseCtl (до этого в течение некоторого времени он назывался Win32 BaseCtl). Поскольку шаблон является примером и не обладает поддержкой со стороны Microsoft, считается, что он был заменен описанными выше средствами разработки элементов на базе ATL. Я не стану подробно рассматривать его. Если только вам не приходится переделывать существующий шаблон, написанный на его основе, я бы не рекомендовал использовать ActiveX BaseCtl для создания элементов (хотя бы из-за отсутствия поддержки). Шаблон имеет собственного мастера. Интересно заметить, что этот мастер написан на Visual Basic. Он генерирует комплект исходных файлов на C++, которые вам приходится изменять для того, чтобы приспособить элемент для своих целей. При этом он старается выбирать имена классов и функций, используемые MFC, чтобы по возможности облегчить переделку существующих элементов. Тем не менее BaseCtl в отличие от MFС-программ не использует схем сообщений, так что вам придется обрабатывать сообщения Windows наиболее традиционным способом — через оконную процедурПервая версия BaseCtl также не учитывала изменений, внесенных спецификациями OCX 96 и ActiveX, хотя соответствующая поддержка была добавлена в начале 1996 года. Нетрудно заметить некоторое сходство между реализацией элементов в ATL и BaseCtl, поскольку ряд идей, впервые появившихся в BaseCtl, был повторно использован (проще говоря — украден) разработчиками ATL.
www.books-shop.com
4.13 Создание элементов ActiveX на языке Java в среде Visual J++ На ранней стадии разработки Visual J++ фирма Microsoft решила как можно аккуратнее объединить миры COM и Java. К счастью, разработчики Java из Sun Microsystems словно заранее подумали о такой возможности. В Java была реализована концепция интерфейсов, и между интерфейсами Java и COM-интерфейсами нетрудно было установить соответствие. Небольшое усовершенствование компилятора позволило программам на Visual J++ пользоваться интерфейсами, определенными в библиотеках типов, и либо реализовать их (то есть выполнять функции сервера), либо пользоваться ими (выполнять функции клиента) так, словно они являются классами Java. Разумеется, как и в любой другой программе для COM, ничто не мешает программе на Visual J++ одновременно действовать в роли как клиента, так и сервера. Впрочем, этим дело не ограничивается. Поддержка COM в Java не сводится к простому расширению компилятора, позволяющему работать с библиотеками типов. Основная работа на самом деле связана с реализацией Microsoft виртуальной машины Java (VM), которая должна поддерживать работу COM в апплетах и приложениях Java. Так выглядит поддержка COM в первой версии Visual J++, появившейся летом 1996 года. Конечно, вы имеете полную возможность написать апплет, который бы работал в HTML-странице или броузере, но при этом мог использоваться как объект Automation другими языками. Рассмотрим пример:
import mytlbs.hexedt32; class Jtest { public void CallMe() { CGateway x; x.Show(); } } Обратите внимание на то, что библиотека типов импортируется (термин означает примерно то же, что и #include) точно так же, как и обычный файл класса Java. Компилятор сначала ищет файл класса и затем, если поиски оказались безуспешными, просматривает библиотеки типов. Кроме того, учтите, что «импортировать» можно все содержимое библиотеки типов или ее отдельные элементы. Например, если бы я ввел строку import mytlbs.hexedt32.CGateway;то компилятор импортировал бы из библиотеки типов только вспомогательный класс CGateway. В этом коротком примере я определяю класс с именем JTest, содержащий открытую функцию CallMe. Функция CallMe создает экземпляр класса CGateway (который, разумеется, представляет собой COM- объект) и вызывает его метод Show. Не видно никаких отличий от стандартов языка Java — потому что их и нет. Основная идея заключается именно в том, что вам не стоит задумываться, что же именно вы создаете — COM-объекты или классы Java. И все же в этой версии Visual J++ трудно написать полноценный элемент (с событиями, страницами свойств, устойчивыми свойствами и т. д.) из-за двух проблем:
Приходится писать большой объем кода. Как описать механизм возбуждения событий на Java?
Microsoft пытается решить эти проблемы за счет расширения существующих Java-библиотек (таких, как AWT — пакет абстрактного окна), при котором VM позволяла бы создавать элементы в обычном коде Java. Повсюду (в том числе и в Sun) разрабатываются схемы, которые помогают обеспечить единый механизм инициирования событий в элементах, написанных на Java. Необходимо, чтобы такой механизм был дружественным по отношению к COM.
www.books-shop.com
4.14 Примечания по поводу примеров, использованных в этой книге В первом издании этой книги использовались примеры, которые вполне естественно были основаны на реализации элементов при помощи MFC. В этом издании рассматривается ряд других, описанных выше способов создания элементов. Пришлось принимать решение — можно было использовать в каждой главе свои программные средства, но мне показалось, что это лишь запутает читателя. С другой стороны, можно было написать каждый пример с использованием всех мыслимых средств, но это внесло бы еще большую путаницу. Затем я решил, что большая часть содержимого книги не представляет интереса для среднестатистического программиста на Visual Basic, так что включить версии всех примеров на Visual Basic было бы неразумно. Полагаю (хотя и не уверен), что сказанное справедливо и для Visual J++. Давайте начистоту: основное внимание в этой книге уделено C++. Поэтому я принял решение (такое случается нечасто, и я отметил этот день в календаре) — во всех примерах элементов в этой книге используется MFC, как и в первом издании. Тем не менее, чтобы пользователи ATL (или аналогичной библиотеки) смогли разобраться в тексте программ или происходящих событиях, я также буду объяснять происходящее в терминах COM, что мне не всегда удавалось сделать в первом издании. По крайней мере, я избавлю вас от необходимости копаться в исходных текстах MFC и выяснять, что же именно творится за кулисами.
www.books-shop.com
Глава
5
Основы элементов ActiveX Свойства Представляю себе заинтригованного читателя, дрожащие от нетерпения руки и горячую мольбу: «Расскажите, расскажите мне о свойствах»! Впрочем, возможно, я слегка преувеличиваю, и все же темой этой главы станут именно свойства и, так сказать, свойства свойств. Мы умеем создавать простейшие элементы ActiveX, знаем, что ClassWizard помогает добавлять свойства для элементов, построенных на базе MFC. Так какие же свойства следует добавить? Давайте вспомним главу 3, в которой мы узнали о трех типах свойств: свойствах элемента, свойствах окружения и расширенных свойствах. «Свойства окружения» поддерживаются клиентским узлом, в который внедряется элемент, контейнер пользуется ими для обмена информацией с элементом, чтобы последний мог унаследовать некоторые характеристики среды, в которой он существует. «Расширенные свойства» поддерживаются контейнером по поручению элемента, и чаще всего для контейнера их значения несущественны и не представляют никакого интереса. В первую очередь нас интересуют «свойства элемента». Так называются те самые свойства, которые реализуются самим элементом. В главе 4 мы узнали, что свойства элементов делятся некоторыми средствами разработки на стандартные (те, которые данное средство реализует за вас) и нестандартные (те, которые вы должны написать самостоятельно). Давайте кратко рассмотрим свойства окружения, определенные в спецификации Элементов ActiveX, а затем — расширенные свойства. После этого можно будет заниматься созданием собственных свойств.
5.1 Стандартные свойства окружения В заголовочном файле OLECTL.H можно найти большую часть стандартных свойств окружения, определенных в виде символических имен и присвоенных им dispid (все остальные свойства можно найти в других заголовочных файлах и документации). Тем не менее в файле не указано, что делает то или иное свойство, поэтому сейчас мы пройдемся по списку и выясним назначение всех свойств. В таблице перечислены все стандартные свойства окружения вместе со значениями их dispid. Большинство свойств взято из OLECTL.H, однако некоторые пришли из спецификаций OCX 96 и ActiveX. Имя*
Имена взяты из различных источников — спецификаций Элементов OLE, OCX 96,Элементов ActiveX, а также заголовочных файлов, входящих в комплект различных SDK.
Как было сказано выше, не следует полагаться на непосредственные значения dispid, поскольку они могут измениться, вместо этого пользуйтесь символическими именами (DISPID_AMBIENT_FONT). Не стоит полагаться и на имена свойств, поскольку в иностранных языках наверняка будут встречаться другие слова. Заголовочный файл OLECTL.H находится в подкаталоге INCLUDE каталога Visual C++, созданного в процессе инсталляции. Чаще всего встречается путь C:\MSDEV\INCLUDE. Если на вашем компьютере установлен пакет Win32 SDK, этот же файл можно найти и в его каталоге, обычно C:\MSTOOLS\INCLUDE. Наконец, если вы установили и ActiveX SDK, то файл будет находиться и в каталоге этого пакета (обычно C:\INETSDK\INCLUDE). Контейнер может добавить в этот список любые свойства окружения по своему усмотрению. Однако в этом случае ими смогут пользоваться лишь элементы, знающие об их существовании. Это означает, что некоторые элементы будут работать с контейнером не так, как другие. Не стоит думать, что это обязательно плохо — просто вы сможете писать элементы, которые пользуются специальными возможностями какого-то определенного класса контейнеров. Из перечисленных выше свойств окружения, которые не рассматривались нами раньше и назначение которых не является очевидным (полагаю, названия свойств вроде BackColor говорят сами за себя), наибольший интерес представляют свойства LocaleID, MessageReflect, TextAlign, SupportMnemonics, AutoClip, Palette и TransferPriority.
При помощи свойства LocaleID контейнер сообщает элементам, в каком локальном контексте они выполняются. Упрощенно можно считать, что локальный контекст определяет разговорный язык. Если значение свойства MessageReflect равно TRUE, контейнер будет «отражать» сообщения Microsoft Windows, посылая их обратно элементу. В основном эта возможность используется для элементов ActiveX, которые представляют собой подклассы стандартных элементов Windows. Делается это потому, что стандартные элементы Windows обычно посылают своим родительским окнам сообщения при выполнении некоторых условий. Например, нажатая кнопка посылает своему родителю сообщение WM_COMMAND с уведомляющим идентификатором BN_CLICKED. Элементы ActiveX, как и их предшественники VBX, используют для передачи информации родителям другой механизм — события, поэтому большинство элементов не посылает сообщений. На случай, если это все же произойдет, типичные библиотеки для разработки элементов создают специальное невидимое окно, которое называется «окном-отражателем» и действует в качестве родительского окна элемента. В MFC такое окно реализуется классом COleControl. Отражатель возвращает полученные сообщения, посылая их обратно элементу-отправителю, и пользуется при этом нестандартными номерами сообщений (символическими константами, которые начинаются с OCM_ и определяются в файле OLECTL.H). Элемент может обработать эти сообщения и поступить с ними так, как положено в ActiveX — например, инициировать событие. Если контейнер готов получать сообщения и возвращать их обратно элементу (а большинство контейнеров к этому не готово), он присваивает свойству окружения MessageReflect значение TRUE, заставляя элемент отказаться от использования своего окна-рефлектора. TextAlign сообщает элементам, как клиентский узел хотел бы выровнять выводимый ими текст. Если значение свойства равно 0, элемент должен следовать «общему» принципу выравнивания: текст выравнивается по левому краю, а числа — по правому. Значение 1 означает выравнивание по левому краю, 2 — по центру, 3 — по правому краю и 4 — по ширине (равномерное заполнение всего свободного места от левого до правого поля).
www.books-shop.com
При помощи свойства SupportMnemonics контейнер сообщает своим элементам, что он поддерживает расширенный клавиатурный интерфейс и может принимать мнемонические сочетания клавиш, предназначенные для элементов. Если в конкретном клиентском узле это свойство окружения недоступно или имеет значение FALSE, элемент вправе предположить, что контейнер не поддерживает эту возможность, и, как следствие, убрать все внешние признаки мнемонических сокращений (например, символ подчеркивания под определенной буквой). Свойство AutoClip показывает, осуществляет ли контейнер автоматическое отсечение элементов. Если его значение равно TRUE, элемент может смело игнорировать параметр lprcClipRect метода IOleInPlaceObject::SetObjectRects. Если данное свойство окружения отсутствует, элемент считает его равным FALSE. Palette содержит HPAL (логический номер палитры) контейнера. Если контейнер поддерживает палитру, только она может быть реализована в качестве основной (в противном случае получивший фокус элемент сможет реализовать другую палитру, и оставшаяся часть контейнера будет выглядеть довольно странно). Элементы, желающие реализовать собственные палитры, должны, таким образом, делать их фоновыми. Тем не менее если контейнер не поддерживает это свойство окружения или возвращает NULL, контейнер не обладает палитрой, и элемент имеет право реализовать свою собственную палитру (если таковая имеется) в качестве основной. TransferPriority сообщает элементу возможный приоритет, с которым должна происходить загрузка его асинхронных свойств.
Элементы не обязаны считаться со свойствами окружения, однако в некоторых случаях это определенно имеет смысл. Некоторые аспекты поведения ваших элементов определяются используемой библиотекой — MFC или другой. Например, если ваш элемент, созданный при помощи MFC, реализует стандартное свойство Font, то его начальное значение будет определяться по свойству окружения Font клиентского узла. Если то или иное свойство окружения не подходит для вашего случая, не пользуйтесь им. Например, рассмотрим элемент со свойством BackColor. Разумеется, элемент может по своему желанию присвоить ему значение свойства окружения BackColor клиентского узла и слиться с окружающим фоном. Тем не менее если элемент хочет выделяться на общем фоне, вряд ли стоит выбирать то же значение BackColor.
5.2 Некоторые расширенные свойства «Расширенными» называются свойства, которые пользователь обычно ассоциирует с элементом, хотя на самом деле они поддерживаются контейнером. Например, к этой категории относится размер и положение элемента, а также его позиция в порядке перебора элементов (tab order). Все эти свойства связаны с «расширенным элементом» — специальным объектом, реализуемым контейнером (обычно посредством объединения). Когда пользователь читает/задает свойство или вызывает метод, первым в работу включается расширенный элемент. Если он узнает свойство или метод, то выполняет соответствующие действия, в противном случае он уступает очередь самому элементу. Элемент может получить указатель на интерфейс IDispatch расширенного элемента и самостоятельно определить значения расширенных свойств. Стандартных расширенных свойств не так уж много. Контейнеры не обязаны реализовывать все стандартные расширенные свойства (и вообще не обязаны реализовывать хотя бы какие-то из них). Свойства элемента не должны иметь имен или dispid, совпадающих с именами или dispid стандартных расширенных свойств. Контейнер может реализовать дополнительные расширенные свойства, переопределяя поведение некоторых свойств элемента. В документации по MFC этот тезис поясняется на примере свойства Enabled, который реализуется большинством отображаемых элементов. Тем не менее элемент знает лишь о том, активен или неактивен он сам. Может случиться так, что активный элемент находится на неактивной форме, так что элемент тоже должен рассматриваться как неактивный. Соответственно, контейнер в таких случаях может предоставить свое собственное свойство Enabled для расширенного элемента. Вероятно, его реализация будет обращаться к реализации элемента, чтобы элемент знал о возникшей ситуации. Расширенные свойства должны иметь dispid в диапазоне от 0x80010000 до 0x8001FFFF. Некоторые из них распределены заранее, хотя в OLECTL.H для них не определены символические константы. В приведенной ниже таблице перечислены стандартные расширенные свойства.
www.books-shop.com
5.3 Стандартные расширенные свойства Имя Name
Dispid
Описание
Определяемое пользователем имя объекта. Например, при вставке элемента в экранную форму Visual Basic присваивает ему имя (например, Text1), 0x80010000 которое раскрывается как свойство элемента и может быть изменено пользователем.
Visible 0x80010007 Показывает, что элемент отображается в контейнере. Parent 0x80010008 Интерфейс Automation формы, в которую внедрен элемент. Cancel 0x80010037
Показывает, что элемент используется как кнопка Cancel для формы, на которой он находится.
Получает значение TRUE для элемента, который в данный момент является Default 0x80010038 кнопкой по умолчанию для формы, и FALSE — для всех остальных элементов.
ПРИМЕЧАНИЕ Спецификация Элементов ActiveX в настоящее время не содержит никаких стандартных расширенных методов или событий. Я уже упоминал о том, что свойства положения и размера являются расширенными; можно добавить к этому списку порядок обхода и свойства-ярлыки. Ярлыки используются в таких языках, как Visual Basic, чтобы закрепить за элементом произвольную строку, заданную пользователем. Различия между этими расширенными свойствами и теми, что приведены в таблице, заключаются в том, что для последних OLE задает конкретные значения dispid. Поскольку эта книга в большей степени посвящена созданию элементов ActiveX, нежели их использованию, мы не будем подробно рассматривать расширенные свойства.
5.4 Свойства элементов Конечно, наибольший интерес для нас представляют свойства элементов — хотя бы потому, что мы сами реализуем их! Свойства элементов могут иметь любые имена, получать любые параметры (при условии, что их типы поддерживаются Automation) и вообще делать все, что вам заблагорассудится. Некоторые свойства элементов реализуются так часто, что в MFC и других библиотеках предусмотрена их стандартная реализация. Более того, такие свойства обладают стандартными именами и зарезервированными dispid, которые не должны использоваться в других свойствах, методах или событиях. На самом деле необходимо различать два понятия:
Заранее определенные dispid для свойств — некоторые идентификаторы диспетчеризации «резервируются» для определенных семантических значений. Если реализовать свойства элемента, удовлетворяющие этой семантике, им будут присвоены соответствующие dispid. Стандартные (заранее реализованные) свойства — свойства, которые так часто присутствуют в элементах, что некоторые библиотеки для создания элементов (особенно MFC) содержат для них стандартную реализацию, которая заметно облегчает их включение в элемент.
Разумеется, вся вторая группа полностью содержится внутри первой. Итак, стандартные свойства определяются инструментом разработчика, использованным при создании элемента, а не самим элементом или архитектурой Элементов ActiveX. Сожалею, что мне приходится разъяснять столь очевидные положения, но они часто становятся источником недоразумений. В приведенной ниже таблице перечислены все стандартные свойства, определенные в файле OLECTL.H, спецификации OCХ 96 и спецификации Элементов ActiveX.
www.books-shop.com
5.5 Стандартные свойства Имя
Dispid
Описание
AutoSize
–500
Если это свойство равно TRUE, элемент изменяет свои размеры на форме так, чтобы отобразить все свое содержимое.
BackColor*
–501
Цвет, используемый для закраски фона элемента.
BackStyle
–502
Определяет, каким должен быть фон элемента — прозрачным или непрозрачным. Если фон прозрачный, то сквозь него видно все, что находится за ним.
BorderColor
–503
Цвет границы элемента.
BorderStyle
–504
Значение свойства определяет тип границы элемента. Может применяться для того, чтобы полностью удалить границу элемента.
BorderWidth –505
Определяет ширину границы элемента.
DrawMode
–507
Если элемент обладает методами рисования (например, Draw), это свойство позволяет задать режим рисования, то есть способ объединения цветов пера и фона.
DrawStyle
–508
Стиль линий, используемых методами элемента при рисовании.
DrawWidth
–509
Ширина пера, используемого методами элемента при рисовании.
FillColor
–510
Цвет заполнения фигур.
FillStyle
–511
Узор заполнения фигур.
Font
–512
Шрифт, используемый элементом для выводимого текста.
ForeColor
–513
Цвет, используемый элементом для вывода текста и рисования.
Enabled
–514
Определяет, активен ли элемент.
HWnd
–515
Логический номер главного окна элемента.
TabStop
–516
Определяет, можно ли выделить элемент посредством перебора (клавишей Tab).
Text
–517
Текст элемента (то же самое, что и Caption).
Caption
–518
Заголовок элемента (то же самое, что и Text).
BorderVisible –519
Определяет, видна ли граница элемента.
Appearance
Определяет внешний вид элемента.
–520
MousePointer –521
Определяет один из стандартных значков Windows, используемых в качестве курсора мыши.
MouseIcon
–522
Если свойство MousePointer равно 99, значение MouseIcon определяет вид курсора мыши.
Picture
–523
Обобщенный графический объект.
IsValid
–524
Определяет, допустимы ли текущие данные, хранящиеся в объекте.
ReadyState
–525
Текущее состояние готовности элемента во время загрузки (возможно — асинхронной).
*
Стандартные свойства, реализуемые библиотекой MFC 4.2 (вошедшей в состав Microsoft Visual C++ версии 4.2), отмечены полужирным шрифтом.
Тот факт, что свойство определено как стандартное, еще не означает, что вы должны неявно поддерживать его через невидимый код MFC и вообще что вы должны поддерживать его. Он всего лишь говорит о том, что если вы захотите поддерживать такое свойство, то сможете обеспечить стандартное поведение, а также сохранить свое время и усилия, воспользовавшись стандартной реализацией. Все остальные свойства элемента являются нестандартными, и вам придется самостоятельно писать код для их поддержки. Возможно, когданибудь мастер начнет улавливать наши пожелания и напрямую генерировать нужный код, а пока нам все же придется немного поработать на клавиатуре.
Сейчас мы изменим элемент First из предыдущей главы и добавим в него несколько стандартных свойств при помощи ClassWizard. Для этого следует открыть файл проекта и вызвать ClassWizard. Перейдите на вкладку OLE Automation, выделите класс CFirstCtrl и нажмите кнопку Add Property. Выберите нужное свойство из списка External Name и нажмите кнопку OK. Повторяйте два последних действия до тех пор, пока не будут добавлены следующие свойства: BackColor, Caption, Enabled, Font, ForeColor и hWnd. Добавив шесть стандартных свойств, постройте проект заново. Проще всего остановиться на ANSI-версии, хотя никто не запрещает вам выбрать кодировку Unicode, если вы работаете с Windows NT. Пока идет построение проекта, давайте взглянем на изменения, внесенные ClassWizard в ODL-файл проекта. В секции для добавления новых свойств появились следующие строки:
[id(DISPID_BACKCOLOR), bindable, requestedit] OLE_COLOR BackColor; [id(DISPID_CAPTION), bindable, requestedit] BSTR Caption; [id(DISPID_ENABLED), bindable, requestedit] boolean Enabled; [id(DISPID_FONT), bindable] IFontDisp* Font; [id(DISPID_FORECOLOR), bindable, requestedit] OLE_COLOR ForeColor; [id(DISPID_HWND)] OLE_HANDLE hWnd; Обратите внимание на то, что пять из новых свойств объявлены как связываемые (bindable) — это означает, что элемент может сообщать контейнеру о факте изменения этих свойств. Кроме того, четыре свойства имеют атрибут requestedit; он говорит о том, что перед изменением такого свойства элемент должен запросить у контейнера разрешение. Таким образом контейнер может запретить элементу произвольно изменять некоторые свойства без согласования с контейнером. Думаю, эта возможность применяется редко, но в некоторых ситуациях она окажется нелишней — например, для элементов текстового редактора, в котором контейнер (собственно редактор) должен полностью контролировать внешний вид документа. Другие изменения в исходных файлах элемента First сводятся к добавлению нескольких макросов в схему диспетчеризации. Поскольку все добавленные свойства являются стандартными, вместо универсальных макросов, рассмотренных в предыдущей главе, используются специализированные:
DISP_STOCKPROP_BACKCOLOR() DISP_STOCKPROP_CAPTION() DISP_STOCKPROP_ENABLED() DISP_STOCKPROP_FONT() DISP_STOCKPROP_FORECOLOR() DISP_STOCKPROP_HWND() Думаю, к этому моменту элемент уже построен и зарегистрирован компилятором. Если вы похожи на меня, то время от времени вы любите пройтись по реестру и удалить пару-тройку ненужных элементов. Почему-то после этого многие программы отказываются работать. Специально для таких, как мы, Microsoft решила, что COM-объекты должны сами регистрировать себя при запуске. Это существенно облегчает чистку реестра! Теперь начинается самое интересное. Для нового элемента нам понадобится подходящий контейнер — например, Visual Basic 4.0 (вообще говоря, подойдет и тестовый контейнер, но вскоре вы оцените гибкие возможности, предоставляемые программируемым контейнером). Если вы используете Visual Basic 4.0, зарегистрируйте элемент командой Tools|Custom Controls. Наш элемент появляется на рабочей панели. Если вы еще не успели ничего изменить, он будет выглядеть, как стандартная кнопка, изображенная на рис. 5-1.
Рис.5-1. Растровое изображение стандартной кнопки, предоставленное OLE ControlWizard Поместите новый элемент на экранную форму. Не пишите никакого кода — просто запустите «программу». Что вы видите? Совершенно верно, ничего нового. Присутствие стандартных свойств никак не сказывается на внешнем облике программы. Наверное, вы и сами можете
www.books-shop.com
объяснить причины такого поведения. Хотя в элементе появились новые свойства, мы никак не используем их. Кроме того, стандартный код рисования, предоставленный OLE ControlWizard:
pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH))); pdc->Ellipse(rcBounds); закрашивает фон элемента белым цветом перед тем, как рисовать эллипс. Тем не менее если просмотреть значения этих свойств во время выполнения программы, выясняется, что они имеют вполне разумные значения. Откуда они берутся? Может, значения по умолчанию предоставляются каким-то runtime-модулем элемента? Такой вариант тоже возможен, однако в большинстве случаев дело обстоит иначе. Чтобы ближе подойти к пониманию происходящего, измените эквивалентные свойства формы Visual Basic, на которую вы поместили элемент (то есть свойства с теми же именами), удалите элемент и вставьте его снова. Просмотрите его свойства — вы увидите, что они приняли те же значения, которые используются на форме. Происходит следующее: Visual Basic берет свойства формы за основу свойств окружения каждого узла, а стандартные свойства элемента читают соответствующие свойства окружения и принимают их значения. Разумеется, сказанное не относится к некоторым стандартным свойствам, таким как Text, Caption или hWnd; для них значения свойств окружения не имеют никакого смысла. Если теперь изменить значение такого свойства и сохранить проект Visual Basic, старое значение заменяется новым. При повторной загрузке проекта свойства получат те значения, с которыми они были сохранены. Проблема устойчивости свойств будет подробно изучена в последующих главах, а пока давайте рассмотрим файл с кодом формы, полученный при сохранении программы Visual Basic. Приведу соответствующие строки из свойств моего элемента First (у вас они могут быть другими):
Begin FIRSTLib.First First1 Height = 1335 Left = 240 TabIndex = 0 Top = 240 Width = 2895 _Version = 65536 _Extentx = 5106 _Extenty = 2355 _StockProps = 79 ForeColor = 16711680 BackColor = 16711935 BeginProperty font {FB8F0823-0164-101B-84ED-08002B2EC713} name = "MS Sans Serif" charset = 0 weight = 700 size = 9.75 underline = 0 ‘False italic = -1 ‘True strikethrough = 0 ‘False EndProperty End Я задал значения свойств ForeColor, BackColor и Font. Обратите внимание на то, что ForeColor и BackColor сохраняются в виде принятых в Visual Basic десятичных чисел, указывающих соотношение красной, зеленой и синей цветовых составляющих, а свойство Font сохраняется как самостоятельный объект с отдельным набором свойств. Единственные изменения, внесенные мной в стандартный шрифт окружения, — полужирное (weight = 700) и курсивное (italic = –1) начертания, а также увеличенный размер символов (size = 9.75).
ЗАМЕЧАНИЕ
www.books-shop.com
Присмотритесь к двум верхним строкам в верхней части окна свойств Visual Basic, когда выделен ваш элемент. Первое свойство, About, вызывает автоматически добавленный метод AboutBox. Попробуйте щелкнуть его, и на экране появится диалоговое окно, которое нам уже приходилось видеть в тестовом контейнере. Вторая строка, Custom, вызывает страницу (-ы) свойств элемента. Мы еще не успели добавить поддержку для страниц свойств, поэтому при выделении этой строки отображается пустая страница свойств элемента, созданная OLE ControlWizard по умолчанию.
5.6 Новые свойства начинают работать Новые свойства должны приносить хоть какую-то пользу. Мы перепишем функцию рисования так, чтобы она читала значения свойств и использовала их. MFC содержит ряд вспомогательных функций, облегчающих работу со стандартными свойствами. Например, функция InternalGetFont класса COleControl обращается к стандартному шрифту, а функция GetBackColor — к цвету фона. Хотя вы можете воспользоваться функцией InternalGetFont для получения шрифтового объекта и последующей работы с ним, чаще всего требуется лишь выбрать шрифт в контексте устройства (DC) элемента на время рисования. Соответственно, для этого в классе COleControl предусмотрена специальная функция SelectStockFont. Новая версия функции OnDraw нашего элемента выглядит следующим образом:
CFont *hfOld = SelectStockFont(pdc); CBrush cbBack(TranslateColor(GetBackColor())); pdc-> FillRect(rcBounds, &cbBack); pdc-> SetTextColor(TranslateColor(GetForeColor())); RECT rcTemp = rcBounds; pdc-> DrawText(InternalGetText(), -1, &rcTemp, DT_SINGLELINE | DT_CENTER, DT_VCENTER); pdc-> SelectObject(hfOld); Следует заметить, что эта функция не так уж сильно отличается от обычных функций рисования MFC, за исключением того, что в ней встречаются функции для доступа к стандартным свойствам, а также используется переданный прямоугольник границ. Последнее обстоятельство достаточно важно: хотя элемент часто отображается в собственном окне, иногда (и в зависимости от контейнера) он будет отображаться как часть окна контейнера. Соответственно, нельзя гарантировать, что точка с координатами (0,0) лежит в области вывода элемента. При рисовании с использованием фиксированных координат вы можете легко испортить внешнюю экранную форму. Использование прямоугольника границ гарантирует, что все рисование будет происходить только в пределах клиентской области элемента. Приведенный выше фрагмент выбирает стандартный шрифт в DC, сохраняя логический номер предыдущего шрифта в hfOld. Это нужно сделать, потому что перед уничтожением графического объекта необходимо позаботиться о том, чтобы он не был выбран в DC. Не оставляйте свои шрифты выбранными в DC, это считается дурным тоном и к тому же приводит к ошибкам — за исключением случаев, когда выполняется оптимизация графического вывода, описанная в предыдущей главе как часть OCX 96. Затем мы создаем кисть, cbBack, которой будет закрашиваться фон элемента. Следовательно, ей нужно присвоить значение свойства BackColor элемента при помощи функции GetBackColor. Обратите внимание на то, что полученное от GetBackColor значение передается конструктору лишь после его обработки функцией TranslateColor. Эта функция преобразует значение типа OLE_COLOR, возвращаемое GetBackColor, к типу COLORREF, который используется конструктором CBrush::CBrush. Далее мы вызываем функцию FillRect для закраски фона элемента, пользуясь при этом прямоугольником границ (см. выше). Цвету текста присваивается значение свойства ForeColor. Как и раньше, функция TranslateColor преобразует значение функции GetForeColor к правильному типу. Другая функция GDI, SetBkMode, задает режим рисования текста TRANSPARENT. Это означает, что фон элемента не должен проявляться перед выводом текста. Если оставить режиму значение по умолчанию (OPAQUE), то перед выводом текста область будет закрашиваться текущим цветом фона. Текущий цвет фона не совпадает со значением свойства BackColor — это цвет, выбранный в DC на данный момент (по умолчанию — белый).
www.books-shop.com
ЗАМЕЧАНИЕ Учтите, что нет абсолютно никаких гарантий, что переданный функции рисования DC обладает атрибутами, которые Windows предоставляет по умолчанию, поскольку другой фрагмент кода мог воспользоваться им перед тем, как передавать OnDraw и задать другие атрибуты. Для элементов, построенных на базе MFC, функция рисования почти всегда вызывается непосредственно из схемы сообщений MFC при получении сообщения WM_PAINT. Следовательно, DC скорее всего будет иметь атрибуты по умолчанию, если только элемент не является UI-активным.
В следующей строке вызывается функция вывода текста DrawText, которая берет текст стандартного свойства Caption функцией InternalGetText и рисует его в центре прямоугольника элемента, для чего ей передаются флаги DT_VCENTER и DT_CENTER. Флаг DT_VCENTER игнорируется при отсутствии флага DT_SINGLELINE, поэтому нам приходится передавать и его. Последняя строка функции вызывает функцию SelectObject, которая заменяет выбранный в DC шрифт тем, который был ранее сохранен в переменной hfOld. Если построить новый элемент и заново вставить его в проект, вы заметите, что фон элемента принимает заданный вами цвет. Если выделить элемент и задать значение свойства Property в окне свойств Visual Basic, то введенный текст будет отображаться с использованием выбранного вами шрифта и цвета.
5.7 Программный доступ к свойствам элемента Большинство контейнеров, рассчитанных на работу с элементами ActiveX, также предоставляет определенную языковую поддержку, благодаря которой можно программировать элементы на языке, определяемом контейнером. Например, в Visual Basic это диалект языка программирования BASIC. Конкретные правила обращения к элементу и его программирование целиком зависят от контейнера. Тем не менее обычно контейнеры следуют принципам Visual Basic: экземплярам внедренных элементов присваиваются имена, а доступ к свойствам и методам организуется контейнером через условную форму языковой записи. В Visual Basic применяется запись вида object.property или object.method, где object — имя экземпляра программируемого элемента. Итак, внедренный в форму Visual Basic экземпляр элемента First обычно получает имя First1. Во время выполнения программы можно задавать значения свойств — например, свойства Caption:
First1.Caption = "I’ve been set programmatically" Если включить эту строку в обработчик события Click кнопки, находящейся на этой же форме, вы сможете в режиме выполнения в любой момент заменить заголовок, заданный в режиме конструирования, содержащейся в кавычках строкой. Для этого будет достаточно нажать кнопку.
5.8 Добавление нестандартных свойств Наделить элемент нестандартными свойствами несложно, однако польза от них ограничена. Обычно элемент становится по-настоящему полезным лишь после того, как вы включите в него свои собственные свойства. «Нестандартные свойства» добавляются точно так же, как стандартные, разве что вам приходится писать несколько больший объем кода. Перед добавлением новых свойств необходимо точно определить, что же должен делать элемент. Программируя для COM, вы будете встречать все больше и больше различных HRESULT. Обрабатывая их в программе, вы сможете понять суть возникших проблем. При получении HRESULT во время выполнения готовой программы нужно выяснить, что же он означает, чтобы при необходимости предпринять какие-либо действия и, возможно, предупредить пользователя. Сейчас мы изменим наш элемент так, чтобы он мог получить HRESULT и посредством свойств вернуть содержательную текстовую строку с его описанием, его статус (например, является ли он фатальным для программы или чисто информационным), компонент, к которому он относится (например, RPC, Win32 или COM), а также сам код ошибки. Для этого необходимо определить соответствующие свойства. В следующей таблице перечислены нестандартные свойства, которые мы определим для элемента First в этой главе. Хотя в нескольких последующих главах элемент будет усовершенствован, к концу этой главы у вас появится рабочий элемент.
www.books-shop.com
Свойство
Тип
Описание
short
Код ошибки из HRESULT (младшие 16 бит). Свойство доступно только для чтения.
ErrorName BSTR
Имя ошибки из директивы #define. Свойство доступно только для чтения.
Facility
BSTR
Код компонента из HRESULT. Свойство доступно только для чтения.
Message
BSTR
Содержательное сообщение, связанное с HRESULT. Свойство доступно только для чтения.
HResult
SCODE Собственно HRESULT.
Severity
BSTR
Code
Код статуса из HRESULT, преобразованный в содержательную строку. Свойство доступно только для чтения.
При виде таблицы немедленно возникает пара вопросов. Во-первых, что такое BSTR? Во-вторых, почему пять из шести новых свойств доступны только для чтения? BSTR — определенный в Automation тип для работы со строками. BSTR на самом деле представляет собой адрес первого символа строки. Как и стандартные строки C и C++, строка должна завершаться нуль-символом. Тем не менее слово, непосредственно предшествующее началу строки, содержит ее длину. Хранение длины вместе со строкой ускоряет вызов функций API для копирования и обработки строк, а также позволяет включать в строку внутренние нулевые байты. Размещение и обработка данных типа BSTR происходит при помощи функций API, являющихся частью системного сервиса Automation. В их число входят функции SysAllocString и SysStringLen. Контроллеры Automation (например, Visual Basic) рассматривают свойства типа BSTR как обычные строки, а библиотека MFC берет на себя большую часть работы по размещению и удалению таких данных. Пять свойств сделаны доступными только для чтения, потому что их значения не должны задаваться напрямую. Поскольку элемент предназначен для разложения HRESULT на составляющие коды, произвольное изменение этих составляющих не имеет никакого смысла. Значения этих свойств пересчитываются при каждой новой установке свойства HResult. Пользователь может их прочитать, но не изменить. Хватит разговоров. Загрузите элемент First в Visual C++ и вызовите ClassWizard. Перейдите на вкладку OLE Automation и выделите класс CfirstCtrl. Добавьте в него шесть свойств из таблицы, учитывая следующее:
Каждое свойство использует схему реализации Get/Set Methods (выбирается в диалоговом окне Add Property). Убедитесь, что для всех свойств, кроме HResult, поле Set Function осталось пустым. В этом случае ClassWizard не генерирует функцию для записи свойства, и оно становится доступным только для чтения.
Хотя мы еще не написали ни одной строки программы, сгенерированный ClassWizard «скелет» будет компилироваться. Убедитесь в этом и постройте проект. Если рассмотреть код, сгенерированный для метода Get любого строкового свойства, вы заметите, что каждый метод состоит из двух строк. В первой из них объявляется переменная типа CString с именем strResult, а вторая возвращает результат вызова метода AllocSysString для этой переменной. Класс MFC CString содержит «оболочку» для функции BSTR API AllocSysString, которая позволяет создавать объекты CString как BSTR. Протестируем только что созданный элемент при помощи Visual Basic. Запустите Visual Basic и включите элемент First в проект (если это не было сделано ранее) командой Tools|Custom Controls. Теперь нарисуйте элемент на форме и, пока он остается выделенным, перейдите в окно свойств. Присвойте переменной HResult произвольно значение. Но позвольте, куда подевалось свойство HResult? Свойства Code, Error, Facility, Message и Severity благополучно присутствуют, так что Visual Basic успешно просмотрел библиотеку типов. Так почему же HResult отсутствует в списке свойств? Давайте пойдем другим путем. Разместите на форме кнопку и добавьте код в обработчик ее события Click. Попробуем присвоить значение свойству HResult элемента First программным способом:
www.books-shop.com
First1.HResult = &H8001FFFF Запустите программу. Как ни странно, она не компилируется — возникает ошибка, которая сообщает о том, что используемый тип не поддерживается Visual Basic. Все понятно: Visual Basic не работает с переменными типа SCODE, а именно этот тип был (вполне логично!) выбран для свойства HResult. Что делать? Разумеется, чтобы от элемента был хоть какой-то прок, нужно придумать способ для присвоения нужного значения HRESULT. Сейчас мы сделаем то, что обычно не рекомендуется в учебниках по программированию — зная, что HRESULT сейчас является длинным целым (32 бита), мы будем хранить его в свойстве типа long. Для этого необходимо первым делом удалить старое свойство. Снова вызовите ClassWizard, выделите свойство HResult и нажмите кнопку Delete. Появляется окно сообщения; в нем сказано, что вам придется удалить реализации CFirstCtrl::GetHResult и CFirstCtrl::SetHResult. Нажмите кнопку Yes. Перейдите к коду и удалите методы GetHResult и SetHResult. Вам не придется изменять схему диспетчеризации, заголовочный файл класса или ODL-файл проекта, поскольку ClassWizard уже внес все необходимые исправления. Единственная причина, по которой он не удалил методы, заключается в том, что удаление кода — слишком ответственная операция. Разработчики Visual C++ решили, что их инструменты вообще не должны удалять пользовательский код из программы. В нашем случае это вызывает некоторые неудобства, но представьте себе, что из вашей программы исчезла большая функция, код которой должен использоваться в новой версии этой функции! Снова добавьте свойство HResult, но на этот раз оно должно иметь тип long. Чтобы успокоить нервы, постройте проект заново и снова протестируйте элемент в Visual Basic. На этот раз свойство HResult присутствует в окне свойств Visual Basic, и вы сможете скомпилировать фрагмент, в котором задается его значение.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Наверное, вы полагаете, что я долго и тщательно продумывал этот поучительный пример. Ничего подобного — просто я столкнулся с этой ошибкой во время создания элемента!
Формат HRESULT определен в файле WINERROR.H, входящем в Microsoft Visual C++ и Win32 SDK. Его структура изображена на рис. 5-2. Мы хотим разбить значение свойства HResult на составные части и преобразовать все значение в строку сообщения. Коды компонента и статуса лучше воспринимаются в виде строк, а не чисел, поэтому их я тоже преобразую в строки. Итак, действия по интерпретации HRESULT должны происходить в следующем порядке: 1. 2.
Присвойте требуемое значение свойству HResult. Получите строку сообщения для данного значения HRESULT, хранящуюся в свойстве Caption. Если строка пустая («»), значение HRESULT не было опознано программой. 3. Если значение HRESULT было опознано, свойство Facility будет содержать название компонента, сгенерировавшего HRESULT. Свойство Code будет содержать код ошибки, а Severity — строку с описанием: «Success» (успех), «Informational» (информационное сообщение), «Warning» (предупреждение) или «Error» (ошибка). Свойство ErrorName будет содержать символическое имя, присвоенное данному HRESULT, а Message — строку сообщения (фактически значение Caption складывается из двух последних свойств).
www.books-shop.com
Рис. 5-2.Структура HRESULT А вот как работает внутренняя логика элемента: 1.
При запуске установить внутренний флаг, означающий «Недопустимое значение HRESULT». 2. Когда свойству HResult будет присвоено значение, проверить его на допустимость. 3. Если значение допустимо, очистить внутренний флаг, в противном случае прекратить работу. 4. Сохранить строку сообщения в свойстве Message, а символическое имя — в свойстве ErrorName. Скомбинировать их, чтобы сформировать значение свойства Caption. 5. При запросе свойства Code и допустимом HRESULT, вернуть младшие 16 бит свойства HResult. 6. При запросе свойства Facility и допустимом HRESULT извлечь из значения HResult поле Facility и загрузить соответствующую строку. Вернуть строку. 7. При запросе свойства Severity и допустимом HRESULT извлечь из значения HResult поле Severity и загрузить соответствующую строку. Вернуть строку. Для работы этого алгоритма нам придется добавить в класс элемента несколько переменных. Откройте заголовочный файл FIRSTCTL.H и добавьте следующие строки в нижнюю часть определения класса:
private: long BOOL CString CString
m_HResult; m_bIsValid; m_csSymbol; m_csMessage;
Переменная m_HResult хранит значение HRESULT, переданное в свойстве HResult. Переменная m_bIsValid определяет, может ли текущее значение m_HResult рассматриваться как допустимый HRESULT. Переменная m_csSymbol хранит символическое имя HRESULT, а m_csMessage — содержательную строку с описанием. Значения первых двух переменных присваиваются в конструкторе класса. Переменные типа CString инициализируются пустой строкой в конструкторе класса CString. Добавьте следующие строки в конструктор CFirstCtrl после вызова InitializeIIDs:
m_HResult = 0; m_bIsValid = FALSE; Большинство функций для получения свойств производит бесхитростные манипуляции со значением HResult (то есть m_HResult). Все методы доступа, за исключением SetHResult, приведены в листинге 5-1. Листинг 5-1. Код всех методов доступа, за исключением SetHResult
long CFirstCtrl::GetHResult() { return m_HResult; } short CFirstCtrl::GetCode() { if (m_bIsValid) {
www.books-shop.com
return short(m_HResult & 0xFFFF); } else { return -1; } } BSTR CFirstCtrl::GetFacility() { CString strResult; short nFacility = IDS_NOVALID_HRESULT; if (m_bIsValid) { nFacility = short((m_HResult & 0x0FFF0000) >> 16); switch (nFacility) { case 0: case 1: case 2: case 3: case 4: case 7: case 8: case 9: case 10: case 11: break; default: nFacility = -1; } nFacility += IDS_FACILITY_NULL; } strResult.LoadString(nFacility); return strResult.AllocSysString(); } BSTR CFirstCtrl::GetSeverity() { CString strResult; short nSeverity = IDS_NOVALID_HRESULT;
BSTR CFirstCtrl::GetMessage() { return m_csMessage.AllocSysString(); } BSTR CFirstCtrl::GetErrorName() { return m_csSymbol.AllocSysString(); } Функция GetHResult всего лишь возвращает текущее значение m_HResult. Перед этим она даже не проверяет его на допустимость. GetCode проверяет HRESULT и затем маскирует старшие 16 бит, возвращая лишь младшие 16 бит величины.
ЗАМЕЧАНИЕ
www.books-shop.com
На самом деле по возвращаемому значению –1 нельзя судить о допустимости или недопустимости HRESULT, поскольку –1 в шестнадцатеричной записи выглядит как 0xFFFF, что вполне может быть допустимым кодом ошибки.
GetFacility выглядит более интересно. Сначала мы объявляем strResult, переменную типа CString, в которой хранится возвращаемое значение, и переменную nFacility целого типа для хранения кода компонента из HRESULT. Строки с именами компонентов хранятся в строковой таблице, входящей в состав ресурсов элемента. Там же хранится строка, возвращаемая для недопустимых HRESULT (добавленные строковые ресурсы перечислены в таблице на рис. 5-3). nFacility инициализируется идентификатором строки, которая представляет недопустимый HRESULT. Из него посредством операции AND с величиной 0x0FFF0000 выделяется код компонента, после чего сдвигом на 16 разрядов вправо он перемещается в нижние 16 бит, после чего преобразуется в короткое целое и сохраняется в nFacility. Если значение HRESULT допустимо, к значению nFacility прибавляется идентификатор первой строки с именем компонента — результат представляет собой индекс строковой таблицы. Наконец, мы загружаем строку из строковой таблицы функцией CString::LoadString, которая возвращается как значение типа BSTR через функцию CString::AllocSysString. Индексирование строковой таблицы работает лишь в том случае, если каждой строке с именем компонента будет присвоен идентификатор, равный сумме IDS_FACILITY_NULL и кода компонента (то есть компоненту NULL, имеющему код 0, должна соответствовать строка с идентификатором IDS_FACILITY_NULL). Если компонент неизвестен, используется строка с идентификатором IDS_FACILITY_NULL – 1. Идентификатор ресурса
Значение
Строка
IDS_NO_FACILITY
101
«Unknown Facility»
IDS_FACILITY_NULL
102
«NULL»
IDS_FACILITY_RPC
103
«RPC»
IDS_FACILITY_DISPATCH
104
«Automation»
IDS_FACILITY_STORAGE
105
«Storage»
IDS_FACILITY_ITF
106
«Interfaces (COM)»
IDS_FACILITY_WIN32
109
«Win32»
IDS_FACILITY_WINDOWS
110
«Windows»
IDS_FACILITY_SSPI
111
«SSPI»
IDS_FACILITY_CONTROL
112
«Controls»
IDS_FACILITY_CERT
113
«Cert»
IDS_SEVERITY_SUCCESS
114
«Success»
IDS_SEVERITY_INFORMATIONAL 115
«Informational»
IDS_SEVERITY_WARNING
116
«Warning»
IDS_SEVERITY_ERROR
117
«Error»
IDS_SEVERITY_HRESULT
118
«The current HRESULT is not valid»
Рис.5-3.Строковые ресурсы, добавленные к элементу First. В таблицу включены возможные коды компонентов и статуса, а также строка для недопустимых значений HRESULT Функция для получения статуса, GetSeverity, работает практически так же, хотя оператор switch в данном случае не нужен — поле статуса занимает всего 2 бита, и все его значения являются допустимыми. Функции GetMessage и GetErrorName возвращают текущие значения своих переменных через CString:: AllocSysString. Теперь нам предстоит более сложная работа. Функция SetHResult, задающая значение свойства HResult, должна определить допустимость переданного ей HRESULT и присвоить свойствам Message, ErrorName и Caption значения строки сообщения, символического имени ошибки и их комбинацию соответственно. Версия элемента First этой главы идет по упрощенному пути и выполняет линейный (а значит, потенциально очень медленный) поиск в файле с кодами ошибок
www.books-shop.com
WINERROR.H. Этот файл поставляется вместе с Visual C++ и входит в Win32 SDK. В последующих главах наш элемент начнет вести себя более разумно. В листинге 5-2 приведен исходный текст функции SetHResult в ее первом воплощении. Листинг 5-2. Функция SetHResult и связанные с ней функции
if (lpszCnt == NULL) { break; } csCompare = szBuf; bFound = (csCompare.Find(_T("// MessageText:")) != -1); } while (bFound == FALSE); if (bFound) { try { // Пропустить пустую строку комментария cfFile -> ReadString(szBuf, 255); // Получить строку (строки) сообщения m_csMessage.Empty(); do { cfFile -> ReadString(szBuf, 255); if (szBuf[3]) { if (!m_csMessage.IsEmpty()) { m_csMessage += _T(" "); } szBuf[_tcslen(szBuf) - 1] = TCHAR(0); m_csMessage += szBuf + 4; } } while (szBuf[3]); // Получить строку кода lpszCnt = cfFile -> ReadString(szBuf, 255); } catch (CFileException *e) { m_csMessage.Empty(); e -> Delete(); return FALSE; } if (lpszCnt == NULL) { m_csMessage.Empty(); return FALSE; } *csLine = szBuf; return TRUE; } return FALSE; } long CFirstCtrl::GetTheCode(CString *csLine) { // Пропустить ‘#define’ int i = 7; // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { return 0; }
www.books-shop.com
// Получить символическое имя m_csSymbol.Empty(); while ((csLine -> GetLength() > i) && !(_istspace(csLine -> GetAt(i)))) { m_csSymbol += csLine -> GetAt(i); ++i; } if (csLine -> GetLength() <= i) { m_csSymbol.Empty(); return 0; } // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { m_csSymbol.Empty(); return 0; } // В последних версиях файла WINERROR.H номер ошибки // может быть представлен в виде _HRESULT_TYPEDEF(номер), // в этом случае следует пропустить имя макроса. int pos; if ((pos = csLine -> Find(_T("_HRESULT_TYPEDEF_("))) != -1) { i = pos + 18; // Длина имени макроса равна 18 } // Получить число CString csNumber; try { csNumber = csLine -> Mid(i); } catch (CMemoryException *e) { m_csSymbol.Empty(); e -> Delete(); return 0; } return _tcstoul(csNumber, NULL, 0); } Сначала мы инициализируем переменную CString «зашитым в программу» полным именем файла WINERROR.H. Не забудьте внести эту строку в строковую таблицу файла ресурсов, присвоить ей имя IDS_HRESULT_FILE и значение C:\\MSDEV\\INCLUDE\\WINERROR.H (или полное имя файла WINERROR.H на вашем компьютере). Затем очищаются два из трех строковых свойств (m_csMessage и m_csSymbol), значения которых будут заданы при успешном вызове функции. Затем мы создаем переменную cfCodes класса CStdioFile. Класс CStdioFile представляет собой оболочку MFC, облегчающую работу с текстовыми файлами. Кроме того, он осуществляет буферизацию и потому обычно ускоряет чтение и запись в файл. Класс CStdioFile в данном случае предпочтительнее базового класса CFile, потому что файл с кодами ошибок является текстовым, а в классе CStdioFile имеются функции для построчного чтения текстовых файлов.
www.books-shop.com
WINERROR.H открывается в текстовом режиме и доступен только для чтения. Если при открытии файла возникает какая-либо ошибка, функция немедленно прекращает работу. В случае успешного открытия мы начинаем в цикле читать строки файла функцией GetNextDefineLine и отбирать из них те, которые содержат код ошибки (вскоре мы увидим, как это происходит). Когда функция GetNextDefineLine находит такую строку, она заносит ее в переменную csLine. Другая функция, GetTheCode, получает код ошибки, соответствующей данной строке. Если он совпадает с кодом ошибки, содержащемся в переданном значении HRESULT, цикл завершается. Кроме того, цикл прекращает работу и в том случае, если GetNextDefineLine возвращает FALSE — это может произойти при возникновении ошибки, в том числе и при достижении конца файла. Если GetTheCode находит код ошибки, переменной m_bIsValid присваивается TRUE, а из переменных m_csSymbol и m_csMessage составляется расширенное описание; в противном случае переменной расширенного описания присваивается стандартная строка, взятая из строковой таблицы. При желании можно заменить ее пустой строкой. В конце своей работы функция SetHResult присваивает значения переменной m_HResult и свойству Caption через SetText. Кроме того, она сообщает элементу о внесении в него изменений, устанавливая флаг SetModifiedFlag, и закрывает файл. Функция Close может инициировать исключение, но мы временно проигнорируем его (поскольку при закрытии файла, открытого только для чтения, исключения крайне маловероятны). Функция GetNextDefine сканирует файл в поисках строк, содержащих коды ошибок. Ее работа основана на том факте, что файл WINERROR.H создается утилитой Message Compiler (MC) и потому имеет строго определенный формат (утилита MC входит в поставку Visual C++ и Win32 SDK). Если этот формат изменится, элемент перестанет работать! Ниже приводится описание формата:
Сначала идет пустая строка комментария, затем идентификатор сообщения, еще одна пустая строка, строка MessageText:, снова пустая строка и наконец строка #define, в которой может присутствовать (а может и отсутствовать) макрос _HRESULT_TYPEDEF_. Для поиска строк, содержащих MessageId:, используется функция CStdioFile::ReadString, которая осуществляет построчное чтение текстового файла, и CString::Find, которая ищет в строке заданную подстроку. ReadString может инициировать исключение, в этом случае происходит выход из цикла. Кроме того, выход из цикла осуществляется и в том случае, если ReadString возвращает NULL, указывая тем самым на достижение конца файла. Цикл прерывается и в случае удачного поиска строки, однако в этом случае переменной hFound присваивается TRUE. Если строка была найдена, мы собираем остальные интересующие нас сведения при помощи нескольких вызовов ReadString. Строка сообщения заносится в m_csMessage — сложность заключается в том, что сообщение может растянуться на несколько строк. Строки необходимо разделять дополнительным пробелом, а признаком конца строки является символ NULL. Кстати говоря, обратите внимание на использование TCHAR(0) вместо символа \0. Дело в том, что \0 является константой типа char, а в кодировке Unicode, с которой вам, возможно, придется работать, нулями должен заполняться весь последний символ массива, а не его первый байт. Кроме того, вместо функции strlen для определения длины строки используется макрос _tcslen, расширение которого зависит от того, определен ли символ _UNICODE. Для кодировки ANSI макрос расширяется в вызов strlen, а для Unicode — в вызов соответствующей функции (впрочем, все это совершенно излишне, поскольку файл WINERROR.H существует только в ANSIкодировке). Ужасно грубый прием, посредством которого текстовая часть строки сообщения заносится в m_csMessage со смещением в 4 байта, основан на том факте, что текст сообщения начинается со смещением в четыре символа после начала строки (два символа комментария, за которыми следуют два пробела). Конечно, для подобных случаев следует определять константы. После получения строки сообщения цикл завершается по обнаружению пустой строки
www.books-shop.com
комментария. Последнее, что происходит в цикле, — получение строки #define. Если при какомлибо вызове ReadString возбуждается исключение, функция очищает строку сообщения и прекращает работу, в противном случае строка #define копируется в переданный объект CString, после чего происходит возврат из функции. Функция GetTheCode извлекает из строки символическое имя и код ошибки. Переменная i используется как счетчик для перебора символов строки. Присвоенное ей начальное значение 7 позволяет пропустить начальные символы #define (вместо 7 также следует определить символическую константу). Затем начинается цикл, в котором пропускаются символы-разделители (white space). Для безопасной (в отношении Unicode) идентификации символо-разделителей используется макрос _istspace. В соответствии с форматом файла дальше в строке должно стоять символическое имя кода, которое заносится в переменную m_csSymbol. Затем мы снова пропускаем символыразделители. Если в строке попадется макрос _HRESULT_TYPEDEF_, мы пропускаем его имя и оставляем только номер ошибки. Просмотр файла WINERROR.H показывает, что одни HRESULT указываются в десятичной, а другие — в шестнадцатеричной системе, поэтому нам понадобится какой-нибудь хитрый способ для чтения чисел. К счастью, существует безопасная в отношении Unicode функция _tctoul (в зависимости от кодировки соответствует strtoul или wctoul). Она преобразует ASCII-строку в длинное целое без знака. Если строка начинается с 0x, она считается шестнадцатеричной, если с 0 — восьмеричной, а любая другая цифра означает десятичное число. Если последний параметр функции равен 0, _tctoul автоматически определяет основание системы счисления для данного числа. Функция GetTheCode возвращает преобразованное значение HRESULT. Две рассмотренные выше функции должны быть объявлены в заголовочном файле класса, поэтому добавьте следующие две строки после объявления закрытых переменных:
BOOL GetNextDefineLine(CStdioFile *cfFile, CString *csLine); long GetTheCode(CString *csLine);
5.9 Построение и тестирование элемента Мы подошли к решающему моменту — построению и тестированию элемента. Проследите за тем, чтобы файл WINERROR.H находился по указанному пути. Во время тестирования элемента не забывайте, что свойство HResult объявлено как длинное целое и потому имеет знак — его значения, превышающие 0x7FFFFFFF, будут рассматриваться как отрицательные. В нашем элементе это не вызовет никаких проблем, поскольку число всегда можно интерпретировать как беззнаковое (если быть абсолютно точным, в нашем коде не возникает ситуаций, при которых это различие будет существенным). Тем не менее свойство HResult может вызвать проблемы как в Visual Basic, так и в тестовом контейнере. Чтобы справиться с бедой (в Visual Basic), введите число как шестнадцатеричное с префиксом &H, чтобы Visual Basic рассматривал его как беззнаковое, или преобразуйте в соответствующее отрицательное число. На рис. 5-4 изображен вид нашего элемента, внедренного в тестовый контейнер, для нулевого значения HRESULT.
www.books-shop.com
Рис.5-4. Элемент First, внедренный в тестовый контейнер Нам удалось построить элемент, обладающий как стандартными, так и нестандартными свойствами, а также заставить его делать нечто отдаленно полезное. Тем не менее наш элемент обладает целым рядом недостатков:
Он предполагает фиксированный формат файла ошибок. Он всегда осуществляет последовательный поиск в файле. Он почти не реагирует на ошибки. Он не умеет декодировать значения HRESULT, отсутствующие в WINERROR.H. Текст элемента выводится в одну строку, что затрудняет чтение длинных сообщений.
По мере знакомства с элементами ActiveX в последующих главах мы исправим все эти недостатки.
5.10 Свойства элементов в других библиотеках Поскольку работа со свойствами элемента происходит через первичный интерфейс диспетчеризации (который, разумеется, может быть двойственным), при добавлении новых свойств к элементу, написанному на C++ без применения MFC, обычно приходится добавлять функции свойств в классическом для COM стиле — с использованием макросов STDMETHOD и STDMETHODIMP (вспомните программу AutoProg из предыдущей главы, в которой была реализована простейшая точка соединения). Кроме того, вам придется отредактировать в файле на языке IDL (или ODL, если вы продолжаете им пользоваться!) сведения об интерфейсе диспетчеризации элемента. Нередко среда разработки содержит специальные средства, которые существенно облегчают такое редактирование. Например, вы можете выбрать интерфейс, тем или иным способом выполнить команду «добавить метод», ввести или выбрать характеристики добавляемого метода — а все изменения в файлах H, CPP и IDL будут выполнены автоматически. И последнее, что осталось сказать о свойствах. MFC ClassWizard позволяет вам выбрать для свойства одну из следующих реализаций:
Переменная класса. Переменная класса с уведомляющей функцией. Функции чтения/записи свойства.
Наверное, вы уже догадываетесь, что на самом деле COM допускает лишь последний из этих трех вариантов. При выборе других вариантов MFC создает вспомогательный код, имитирующий выбранную реализацию.
www.books-shop.com
Если вы хотите сделать свойство доступным только для чтения (или только для записи — но такое встречается очень редко), не реализуйте функцию записи put (или функцию чтения get для свойств, доступных только для записи). Это, однако, приведет к возникновению ошибки Automation «функция не найдена», так что вам стоит все равно реализовать функцию put и включить в нее код, единственное назначение которого — инициировать исключение «функция put не поддерживается». Если ваш элемент написан не на C++, а на другом языке, добавление свойств обычно происходит проще. Например, на Visual Basic достаточно объявить новое свойство и написать для него нужный код. На Microsoft Visual J++ вы просто включаете в класс элемента новые методы чтения и записи и реализуете их.
www.books-shop.com
Глава
6
Устойчивость свойств: сериализация В этой главе рассматривается концепция «устойчивости свойств». Под «устойчивостью» понимается возможность сохранения свойств после завершения работавшей с ними программы. Представьте: вы создали программу, которая использует элемент ActiveX, и потратили много времени на настройку свойств элемента. Согласитесь, было бы крайне неприятно запустить программу на следующий день и увидеть, что все подобранные значения свойств куда-то исчезли. Так как же убедить их остаться? Ответ вы найдете в этой главе.
Подготовка Если внедрить элемент First из главы 5 в экранную форму Microsoft Visual Basic версии 4.0, задать значения свойств и сохранить форму, в FRM-файле появится текстовое представление формы и ее содержимого. Файл содержит значения всех свойств для каждого элемента на форме. Тем не менее вам может показаться, что на самом деле сохраняются лишь отдельные свойства вашего элемента. Например, секция FRM-файла для элемента First может выглядеть так:
Begin FirstLib.First First1 Height = 1695 Left = 120 TabIndex = 0 Top = 120 Width = 4215 _Version = 65536 _ExtentX = 7435 _ExtentY = 2990 _StockProps = 79 Caption = "Hello 32-bit VB4!" ForeColor = 255 BackColor = 16711680 BeginProperty Font {FB8F0823-0164-101B-84ED-08002B2EC713} name = "Algerian" charset = 1 weight = 400 size = 20.25 underline = 0 ‘False italic = 0 ‘False strikethrough = 0 ‘False EndProperty End Из этого списка нам знакомы разве что свойства Caption, ForeColor и BackColor. Интересно заметить, что все они являются стандартными свойствами MFC. Добавленные нами свойства (например, HResult или Message) в списке отсутствуют. Кроме того, Visual Basic сохранил вместе со стандартными и расширенные свойства элемента (например, Height или TabIndex). Наверное, вы понимаете, что сам элемент не создает и не читает FRM-файл напрямую, а Visual Basic не запоминает значений всех свойств элемента и поэтому не может отвечать за их сохранение. Давайте вспомним, что нам известно о теоретических положениях устойчивости элементов. Прежде всего контейнер должен решить, какую устойчивость он будет требовать от элемента: обычную, двоичную или нечто третье, называемое «сохранением в текстовом формате». В первом случае он обычно обращается к методам интерфейса IPersistStreamInit элемента. Тем не
www.books-shop.com
менее старые контейнеры могут не знать о существовании этого интерфейса и пользоваться IPersistStorage. Существуют и другие интерфейсы семейства IPersistxxx, которые могут поддерживаться элементом и к которым может обращаться контейнер. Поддерживая эти интерфейсы, элемент облегчает работу контейнеру, ориентированному на их использование, однако при этом размер элемента несколько увеличивается — вам придется также реализовать как минимум IPersistStorage и (желательно) IPersistStreamInit. Если контейнер захочет организовать сохранение в текстовом формате, у него появляется выбор. Старые элементы (следовательно, и старые контейнеры) через интерфейс IDataObject предоставляют наборы свойств, состоящие из пар имя/значение свойства. Вы можете поддержать этот механизм и в своем элементе, однако он не слишком эффективен, так что большинство элементов предпочитает пользоваться интерфейсом IPersistPropertyBag. Он позволяет вызывающей стороне определить, для каких устойчивых свойств она желает получить значения (для всех или только для изменившихся), тогда как набор свойств всегда работает сразу со всеми устойчивыми свойствами.
ЗАМЕЧАНИЕ Возможно, вы обратили внимание, что я еще ничего не сказал об асинхронных свойствах. В этой главе также не рассматривается устойчивость свойств в HTML-потоках. Тем не менее обе темы исключительно важны для работы элементов ActiveX в Internet-приложениях и подробно рассматриваются в главе 13, «Элементы ActiveX и Internet». Механизмы, на которых основаны различные типы устойчивости, работают прямолинейно, хотя в них встречаются нетривиальные моменты. Механизм IPersistStorage работает следующим образом: 1.
Контейнер получает указатель на интерфейс IPersistStorage элемента через QueryInterface. 2. Контейнер получает или создает IStorage. 3. Контейнер вызывает метод Save или Load интерфейса IPersistStorage элемента, передавая ему указатель на IStorage. 4. Элемент записывает информацию в хранилище или считывает ее, соответственно создавая или читая потоки. Механизм IPersistStreamInit не отличается от описанного выше, за исключением того, что контейнер передает IStream вместо IStorage, а элемент не создает потоков в хранилище. Механизм IPersistPropertyBag работает следующим образом: 1. 2. 3. 4.
Контейнер получает указатель на интерфейс IPersistPropertyBag элемента через QueryInterface. Контейнер получает указатель на свой собственный интерфейс IPropertyBag, а также на интерфейс IErrorLog (если он захочет реализовать его). Контейнер вызывает метод Save или Load интерфейса IPersistPropertyBag элемента, передавая ему указатели на свои интерфейсы IPropertyBag и IErrorLog (интерфейс протокола ошибок IErrorLog передается только методу Load). Для каждого устойчивого свойства элемент вызывает метод комплекта свойств IPropertyBag::Read или IPropertyBag::Write. Если элемент сохраняет значение свойства, он помещает его в переменную типа VARIANT. Это дает владельцу комплекта свойств большую свободу в выборе способа локального сохранения значения.
Если сохраняемое свойство само является объектом (например, шрифтовым или графическим), владелец комплекта свойств обращается к объекту с требованием сохраниться либо традиционными средствами IPersistStream (и т. д.), либо через другой интерфейс IPersistPropertyBag. Сохранение объектов при этом выполняется достаточно эффективно, поскольку оно позволяет избежать ненужного копирования данных. Вместо того чтобы копировать данные из объекта в элемент, а потом из элемента в контейнер, данные копируются напрямую. Элемент полностью устраняется из этой цепочки.
www.books-shop.com
Чтобы добавленные вами свойства могли сохраняться и читаться в Visual Basic, необходимо обеспечить их сохранение комплектом свойств элемента. Для других контейнеров, не поддерживающих комплекты свойств, необходимо обеспечить их сохранение в наборе свойств элемента, который раскрывается элементом через интерфейс IDataObject. Если загрузить элемент First в тестовый контейнер и выполнить для него команду File|Save Property Set, на диске создается новый файл TMP.DFL. Он будет находиться в том каталоге, который тестовый контейнер считает текущим (я не берусь предсказать, где именно, но это может быть каталог с двоичным представлением элемента First или каталог, из которого был запущен тестовый контейнер — обычно C:\MSDEV\BIN). TMP.DFL является файлом структурного хранения, его содержимое можно просмотреть утилитой DocView, входящей в Visual C++. Выполняемый файл утилиты называется DFVIEW.EXE. Запустите его и просмотрите временный файл, созданный тестовым контейнером. Раскройте все потоки внутри хранилища, чтобы увидеть содержимое всего файла. У вас должно получиться нечто похожее на рис. 6-1. И снова беглый взгляд показывает, что в этом файле сохранены только стандартные свойства.
Рис.6-1.Дамп набора свойств, сохраненного тестовым контейнером для элемента First из главы 5 Итак, после выбора механизма, обеспечивающего устойчивость, необходимо решить, какие именно свойства вашего элемента должны быть устойчивыми. Бессмысленно сохранять динамические свойства, значения которых пересчитываются заново при каждом обращении к ним (в частности, это относится к свойствам Code, Facility и Severity, значения которых вычисляются по текущему значению HResult во время вызова функций доступа). Но что можно сказать об остальных свойствах? Есть ли смысл сохранять свойства Message и ErrorName? Ситуация осложняется тем, что значение свойства Caption строится из значений Message и ErrorName; Тем не менее поскольку Caption является стандартным свойством, оно сохраняется и без вашего участия. Если же учесть то обстоятельство, что вам приходится хранить внутренний флаг m_bIsValid, по которому можно судить о допустимости текущего значения HResult, немедленно возникает несколько вопросов:
Если сохранять свойство HResult, то как в момент его загрузки присвоить значение флагу m_bIsValid? Если предыдущая проблема будет решена, вам придется перезаписать все сохраненные значения остальных свойств. Если же она не будет решена, но вы все же сохраните остальные свойства, то их значения станут недействительными, поскольку свойство HResult не было признано допустимым.
www.books-shop.com
Давайте подумаем, как справиться с этими проблемами. Если удастся заставить контейнер сообщать элементу о загрузке свойств, вы сможете вызвать функцию SetHResult, чтобы определить допустимость восстановленного значения HResult и должным образом задать значения остальных свойств. Другое, более интересное решение — отказаться от сохранения свойств, которые могут вызвать проблемы. На какой бы стратегии вы ни остановились, прежде всего необходимо выбрать механизм обеспечения устойчивости, а затем — свойства, которые должны быть сделаны устойчивыми.
6.2 Устойчивость свойств (с использованием MFC) Почти вся функциональность элементов ActiveX в библиотеке MFC сосредоточена в классе COleControl. Пока что мы пользовались этим классом слепо, не вникая в подробности. Впрочем, сейчас я не собираюсь давать подробных пояснений, а лишь расскажу, как в нем реализована устойчивость свойств. Когда контейнер запрашивает у элемента ActiveX его устойчивые свойства, через интерфейс двоичной устойчивости (IPersistStorage и т. д.) или сохранения в текстовом формате (комплекты или наборы свойств), в конечном счете вызывается функция COleControl::DoPropExchange, выполняющая сериализацию свойств элемента. OLE ControlWizard переопределяет эту функцию за вас, так что ваш класс, производный от COleControl, содержит свою собственную версию DoPropExchange, которая вызывается вместо версии базового класса. Вспомните главу 4 и наше первое знакомство с элементами, созданными на базе MFC; мы рассматривали созданную мастером функцию DoPropExchange, которая состояла всего из двух строк:
ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); Первая строка сохраняет номер текущей версии, а вторая вызывает реализацию функции DoPropExchange базового класса. Последняя вызывает COleControl::ExchangeExtent для сериализации габаритов (размера) элемента, и функцию COleControl::ExchangeStockProps для преобразования его стандартных свойств.
ЗАМЕЧАНИЕ Функция DoPropExchange применяется как для сериализации свойств (записи из элемента на требуемый носитель), так и для десериализации (чтения из ранее сохраненного набора). Кроме того, она вызывается (см. выше) и при получении от контейнера запроса на сериализацию или десериализацию свойств элемента через один из интерфейсов семейства IPersistxxx, поддерживаемых элементами в MFC (вы можете добавить к ним свои собственные интерфейсы).
Для сериализации ваших свойств необходимо добавить код в эту функцию, который будет выполняться при каждом сохранении или загрузке свойств элемента. MFC содержит ряд вспомогательных функций, облегчающих эту задачу. Имена всех этих функций начинаются с PX_, поэтому они известны под обобщающим названием «PX-функции». Сокращение PX происходит от термина «property exchange», то есть «обмен свойств». Часть имени, следующая за символом подчеркивания, соответствует типу данных, с которым работает данная PX-функция. Например, функция PX_Bool используется для сериализации свойств типа BOOL. Первые три параметра совпадают для всех PX-функций:
Указатель на объект CPropExchange, переданный функции DoPropExchange и определяющий контекст сериализации (например, что же именно выполняется — чтение или запись свойств). Название свойства, для которого вызвана данная PX-функция. Переменная (или ссылка, или указатель, в зависимости от типа), в которой хранится записываемое значение или будет храниться прочитанная величина.
Большинство PX-функций также получает четвертый параметр — значение по умолчанию, которое должно быть использовано в том случае, если процесс сериализации по какой-либо причине закончится неудачей. Более сложные PX-функции (например, используемые для
сериализации указателей на COM-интерфейсы) получают и другие параметры, зависящие от типа — например, идентификатор интерфейса. Чтобы обеспечить сериализацию свойства HResult, следует воспользоваться функцией PX_Long:
PX_Long(pPX, _T("HResult"), m_HResult); Кроме того, можно задать для свойства значение по умолчанию на случай неудачной сериализации:
PX_Long(pPX, _T("HResult"), m_HResult, 0); В настоящий момент HResult является единственным нестандартным свойством нашего элемента, значение которого может быть изменено пользователем, поэтому вполне логично, что поддержка устойчивости предусматривается лишь для этого свойства. Тем не менее по указанным выше причинам этого может быть недостаточно. Стандартное свойство Caption будет сохраняться, если не изменить поведение DoPropExchange по умолчанию. Если вы захотите сохранять свойство HResult, то вам придется позаботиться о соответствующей установке флага m_bIsValid. Если не сохранять его, то придется очищать свойство Caption, иначе его значение будет непоследовательным по отношению к HResult. Что делать? Поскольку мы обеспечиваем сериализацию HResult, необходимо также найти способ инициализировать m_bIsValid и задать значения всех свойств, зависящих от HResult. Логичнее всего сделать это в функции DoPropExchange, немедленно после чтения свойства HResult. Но как определить, что выполняется со свойством — сериализация или десериализация? Класс CPropExchange содержит функцию IsLoading, которая возвращает TRUE, если объект класса в данный момент находится в режиме «загрузки свойств из контейнера». Соответственно, функция DoPropExchange должна выглядеть так:
Теперь наш элемент будет сохранять и загружать свойство HResult, причем во время загрузки оно будет проверяться на допустимость, а также будут устанавливаться значения других свойств и внутренних переменных. Конечно, вы можете возразить, что следует изменить код для того, чтобы он перестал сохранять свойство Caption, однако это будет намного сложнее простой перезаписи его при каждой загрузке, что происходит в нашем варианте. Чтобы понять, как работает вся эта механика, давайте проследим за вызовом комплекта свойств от начала до конца. Прежде всего, контейнер вызывает метод IPersistPropertyBag::Load элемента. Текст метода можно найти (по крайней мере, в MFC версии 4.2) в файле CTLPBAG.CPP под именем COleControl:: XPersistPropertyBag::Load (MFC реализует COM-интерфейсы в виде вложенных классов C++; XPersistPropertyBag — имя класса, вложенного по отношению к COleControl и реализующего IPersistPropertyBag). Этот метод создает объект типа CPropbagPropExchange и передает его функции DoPropExchange элемента. Если ему это удается, метод сообщает контейнеру через интерфейс IPropertyNotifySink о том, что некоторые из его свойств изменились (передавая DISPID_ UNKNOWN), и затем выполняет некоторые служебные действия. Итак, вся суть происходящего заключается в объекте CPropbagPropExchange и функции DoPropExchange элемента. Класс CPropbagPropExchange вместе с функциями определен в том же файле. Прежде чем рассматривать его, следует понять, что же делает функция COleControl::DoPropExchange. Фактически она представляет собой набор PX- функций, по одной для каждого устойчивого
www.books-shop.com
свойства. Вся работа простых PX-функций (таких, как PX_Long) сводится к вызову функции ExchangeProp для переданного объекта px (в нашем случае px является объектом типа CPropbagPropExchange). Это приводит к вызову метода IPropertyBag::Read, среди параметров которого — имя свойства и инициализированная переменная типа VARIANT, в которой должно быть сохранено его значение, а также указатель на интерфейс IErrorLog контейнера. Когда метод успешно завершится, значение свойства копируется из VARIANT в переменную, где оно должно храниться. В случае сохранения все происходит практически также, за исключением того, что вызывается метод IPropertyBag::Write, а значение свойства перед вызовом Write копируется в переменную типа VARIANT.
6.3 Другие PX-функции Помимо PX-функций, работающих с простейшими типами, существует функция PX_Blob, которая позволяет делать устойчивыми BLOB-свойства. Под термином «BLOB» понимается двоичный большой объект, или, проще говоря, «объемистые двоичные данные, смысл которых понятен только их создателю!» Главное отличие PX_Blob от всех остальных PX-функций заключается в том, что PX_Blob при чтении свойства выделяет область памяти для объекта, тогда как остальные PX-функции записывают полученное значение в существующую переменную, переданную при вызове. Память выделяется оператором C++ new, поэтому при завершении работы с объектом его владелец должен освободить память соответствующим оператором delete. Функция PX_Font также отличается от других PX-функций тем, что она получает необязательный параметр — шрифтовой объект, из которого должны быть взяты характеристики шрифта по умолчанию, если свойство не будет найдено после десериализации. Обычно шрифтом по умолчанию является шрифт, указанный в свойстве окружения клиентского узла, это позволяет при отсутствии явно заданного шрифта присвоить ему те же атрибуты, которые имеет шрифт в свойстве окружения.
6.4 Устойчивость стандартных свойств Если внимательно изучить информацию, сохраняемую для элемента First в форме Visual Basic, можно найти некоторые новые, не упоминавшиеся ранее свойства. Помимо стандартных, расширенных и одного нестандартного свойства сохраняются значения _Version, _ExtentX, _ExtentY и _StockProps. Эти свойства сохраняются самим элементом с тем, чтобы обеспечить их правильное восстановление при загрузке всех остальных свойств. Величина _Version соответствует номеру версии элемента. Она представляет собой длинное целое: старшие 16 бит равны основному (major) номеру версии, а младшие 16 бит — дополнительному (minor). Если вы не изменяли код элемента, сохраняемое значение будет равно 65536, или 0x10000 в шестнадцатеричном представлении. Номер основной версии в этом значении равен 1, а номер дополнительной — 0, то есть версия равна 1.0. Свойство _Version сохраняется функцией COleControl::ExchangeVersion во время вызова DoPropExchange. Сохраняя номер версии среди свойств элемента, вы облегчаете его работу при загрузке свойств, сохраненных более ранней версией. Некоторые свойства элемента могут отсутствовать в ранней версии, или, наоборот, оказаться лишними. Проверяя номер версии, ваш код функции DoPropExchange может присвоить отсутствующим свойствам разумные значения по умолчанию. При чтении свойств функцией DoPropExchange вызов ExchangeVersion записывает полученный номер версии в объект CPropExchange. В дальнейшем его можно проверить на программном уровне. Свойства _ExtentX и _ExtentY представляют собой размеры элемента в единицах HIMETRIC. Элементы ActiveX пользуются этими значениями для того, чтобы восстановить свои размеры на момент сохранения. Сохранение и восстановление размеров производится функцией ExchangeExtent, вызываемой из COleControl::DoPropExchange. Функция COleControl::ExchangeStockProps обеспечивает поддержку устойчивости для каждого из стандартных свойств, для которых это действительно необходимо. Такие стандартные свойства, как hWnd, не делаются устойчивыми — бессмысленно сохраняет значение, которое может измениться (и наверняка изменится), между различными сеансами работы. На самом деле эта функция занимается сразу всеми устойчивыми стандартными свойствами, что делает ее достаточно сложной, или, точнее — длинной. Тем не менее элемент не обязан делать устойчивыми все стандартные свойства. В нашем случае используются лишь пять устойчивых
www.books-shop.com
стандартных свойств (Caption, BackColor, ForeColor, Font и Enabled). Как же функция узнает, что сохранять или загружать нужно только эти свойства? Из листинга можно узнать, что среди прочих свойств, сохраняемых в FRM-файле Visual Basic, присутствует _StockProps. Данное свойство представляет собой «маску стандартных свойств», при помощи которой MFC выделяет стандартные свойства, используемые элементом. Маска интерпретируется как набор битов, каждый из которых соответствует конкретному стандартному свойству (например, 1 представляет свойство BackColor). Определенные на текущий момент биты маски можно найти в файле CTLPROP.CPP, находящемся в каталоге \MSDEV\MFC\SRC. Так, STOCKPROP_BACKCOLOR определяется как 0х00000001. Наша маска стандартных свойств равна 79, или 04F в шестнадцатеричном представлении. Она указывает ExchangeStockProps на необходимость сохранения или восстановления лишь свойств, биты которых включены в маску. Как видите, MFC заметно упрощает поддержку устойчивости элементов — как двоичной, так и при сохранении в текстовом формате. Вам остается лишь решить, какие свойства должны быть устойчивыми и какие действия следует предпринимать при их восстановлении. Все остальное сводится к простому кодированию.
6.5 Устойчивость свойств (без использования MFC) Программистам, которые пишут элементы ActiveX на C++, не пользуясь MFC, приходится реализовывать соответствующие интерфейсы и писать код для сохранения и загрузки устойчивых свойств элемента. Например, чтобы ваш элемент поддерживал IPersistPropertyBag, вам придется реализовать этот интерфейс (что, впрочем, не особенно сложно) и затем написать код, который каким-то образом отличает устойчивые свойства от прочих, чтобы при вызове IPersistPropertyBag читались и записывались значения только этих свойств. Тем не менее для элементов на базе ActiveX Template Library (ATL) 2.0 эта задача существенно упрощается. Вы сначала решаете, какие интерфейсы устойчивости должны поддерживаться вашим элементом, а затем создаете класс элемента посредством множественного наследования от них. Затем вам остается лишь объявить свойство устойчивым при помощи макросов ATL. В следующей главе рассматривается другая функциональная категория, жизненно необходимая для работы элементов, — методы.
www.books-shop.com
Глава
7
Методы Если вы привыкли иметь дело с нестандартными элементами Microsoft Visual Basic (проще говоря — с VBX), содержащаяся в этой главе информация наверняка покажется вам интересной, так как в модели VBX не были предусмотрены «нестандартные методы». Под методом понимается запрос к объекту на выполнение каких-либо действий. Классические примеры методов — запрос к объекту базы данных с требованием сохранить себя в базе или обращение к кнопке с требованием обработать щелчок мыши. Поскольку VBX не поддерживали нестандартные методы, некоторые разработчики пользовались так называемыми «свойствами действий», то есть свойствами элемента, изменение значений которых приводило к выполнению нужных действий. Разумеется, свойства действий в какой-то мере позволяли имитировать нестандартные методы в Visual Basic, однако они смущали тех пользователей, которые справедливо считали свойства атрибутами объекта, а не «детонаторами» для выполнения действий.
7.1 Элементы ActiveX и нестандартные методы Спецификация ActiveX позволяет добавить нестандартные методы в любой элемент, причем методы, как и свойства, могут возвращать любой из стандартных типов, поддерживаемых Automation. Нестандартные методы могут получать произвольное количество параметров таких типов, хотя в MFC количество параметров метода ограничивается 15 (не дай Бог дожить до такого огромного количества!). Поддержка нестандартных методов в элементах ActiveX означает, что разработчики могут отказаться от использования свойств действий. Впрочем, если вы конвертируете существующий VBX и желаете сохранить его интерфейс, свойства действий все же придется оставить. Некоторые элементы ActiveX, которые приходят на смену существующим VBX, поддерживают свойства действий в целях совместимости, однако при этом они обладают нестандартными методами, которые должны использоваться новыми программами вместо старых свойств. В MFC новые методы добавляются так же легко, как и новые свойства. Добавление новых методов в ClassWizard происходит на той же вкладке OLE Automation. Кроме того, MFC поддерживает два стандартных метода. Если вы захотите поддерживать их в своем элементе, вам не придется писать ни единой строчки кода. Методы DoClick и Refresh не получают параметров и не возвращают никакого результата. DoClick имитирует щелчок левой кнопки мыши на элементе и оказывается особенно полезным для элементов, выполняющих функции кнопок. Он вызывает функцию COleControl::OnClick, а в случае, если элемент поддерживает стандартное событие Click (см. следующую главу) — инициирует это событие. Refresh вызывает функцию COleCOntrol:: Refresh, которая перерисовывает элемент. Этот метод может пригодиться для того, чтобы пользователь мог в любой момент перерисовать элемент.
7.2 Добавление нестандартного метода в элемент на базе MFC Лучший способ объяснить что-нибудь — показать, как это «что-нибудь» применяется на практике. Мы добавим пару нестандартных методов в элемент First, рассматриваемый на протяжении трех последних глав. Кроме того, будет добавлен и стандартный метод Refresh на случай, если кому-нибудь из пользователей вдруг захочется перерисовать элемент по своему желанию. Что будут делать нестандартные методы? Один из главных недостатков текущей реализации элемента First заключается в том, что в ней всегда происходит последовательный просмотр файла ошибок. Если к тому же вспомнить, что имя файла ошибок «зашито» в приложении и в
www.books-shop.com
него нельзя вставить новые определения HRESULT, становится совершенно ясно — наш элемент обладает рядом ограничений, от которых хотелось бы избавиться.
7.3 Простейшая база данных для HRESULT В идеальном мире все определения HRESULT, с которыми работает элемент, должны быть сведены в базу данных. Сейчас мы создадим такую базу, однако она будет не очень сложной, поскольку мы по-прежнему будем рассматривать ее как файл (в главе 10 мы реализуем настоящую поддержку работы с базой данных средствами ODBC — открытой архитектуры баз данных). С форматом базы данных все просто: нам известно, что набор допустимых значений кодов компонента и статуса определен заранее. В нем остается много места для новых кодов, которые также являются потенциальными кандидатами для нашей базы, но до разработки более сложного элемента стоит ограничиться фиксированным набором. Вся информация, которую действительно необходимо хранить для каждого HRESULT, — 32-разрядное значение HRESULT, его символическое имя и связанная с ним строка сообщения. Значение HRESULT стоит сделать ключевым, поскольку оно заведомо является уникальным, а его числовая природа облегчает процедуру поиска. Пока мы будем продолжать работу с файлом при помощи последовательного доступа. Осталось ответить на ряд вопросов:
Поскольку символическое имя и строка сообщения не имеют фиксированной длины, стоит ли выделять для них область максимальной длины для упрощения работы? Или все же пойти по пути повышения гибкости? Следует ли хранить записи базы отсортированными по HRESULT — это упрощает поиск, но усложняет вставку новых записей? Следует ли хранить все три информационных поля в одной записи, или же держать значения HRESULT в отдельном файле вместе с данными о смещении других полей в другом файле (файлах)? Стоит ли усложнять себе жизнь и разрешать удаление записей? Как избавиться от грязной работы по внесению всех записей в WINERROR.H?
Первые три вопроса взаимосвязаны: если нам удастся удачно ответить на первый, то разобраться с остальными будет легче. Я постараюсь по возможности упростить задачу и не стану поддерживать удаление. Мы напишем специальный метод «массовой загрузки», который будет брать все записи из существующего файла и загружать их в базу данных. Разумнее всего иметь файл со значениями HRESULT вместе со смещениями внутри другого файла, в котором хранятся символические имена и строки сообщений. Для повышения производительности (в данном случае — скорости) файл HRESULT будет загружаться в память в начале работы элемента. Это не будет связано с особыми расходами, поскольку не так уж много программ захочет одновременно пользоваться таким элементом, а количество различных HRESULT вряд ли выйдет за пределы разумного. Будем надеяться, что наши расчеты оправдаются (на момент написания книги размер индексного файла составлял около 8 Кб — по сравнению с объемом всего остального, что загружается в системе, эта величина практически незаметна). Конечно, мы все равно живем в 32-разрядном мире (а вы?), так что проблема с нехваткой памяти стоит уже не так остро, как в старые добрые 16-разрядные дни. Итак, приступаем…
7.4 Структура базы данных HRESULT Файл HRESULTS.IND, в дальнейшем именуемый «индексный файл», содержит запись для каждого внесенного в него HRESULT. Запись имеет длину 8 байт. В первых 4 байтах содержится само значение HRESULT, а в оставшихся 4 — величина смещения внутри другого файла, HRESULTS.MSG, где хранятся сведения о HRESULT. Файл HRESULTS.MSG, или «файл сообщений», также содержит запись для каждого HRESULT, занесенного в индексный файл. Запись состоит из строки с символическим именем, заканчивающейся нуль-терминатором,
www.books-shop.com
за которой следует строка сообщения и еще один нуль-терминатор. Индексный файл сортируется по значению HRESULT. На рис. 7-1 показано графическое представление форматов индексного файла с файлом сообщений. Мы создадим метод Add, который добавляет новый HRESULT в базу данных. Ему передаются три параметра: HRESULT, символическое имя и строка сообщения. При удачном добавлении метод должен возвращать TRUE, а если по каким-либо причинам добавление не состоится — FALSE. Второй метод, BatchLoad, получает всего один параметр — имя файла с произвольным количеством записей HRESULT в том же формате, как и в WINERROR.H. BatchLoad возвращает длинное целое, равное количеству HRESULT, загруженных из файла и внесенных в базу данных. В обоих случаях после внесения новых данных происходит сохранение индексного файла и файла сообщений. В случае BatchLoad это происходит после добавления всех HRESULT из файла. Мы не будем усложнять работу с файлами и организовывать транзакции, которые позволили бы нам гарантировать постоянную синхронизацию индексного файла с файлом сообщений — для нашего простого элемента это будет излишней роскошью. Прежде всего необходимо добавить в элемент все три метода. Загрузите файл проекта в Microsoft Visual C++, вызовите ClassWizard и перейдите на вкладку OLE Automation. Выделите в списке имя класса CFirstCtrl и нажмите кнопку Add Method. Выберите из списка External Name стандартное свойство Refresh и нажмите кнопку OK. Затем добавьте метод Add, выберите тип возвращаемого значения BOOL и укажите три параметра:
HResult типа long, Symbol типа LPCTSTR, Message типа LPCTSTR.
Рис. 7-1. Формат индексного файла и файла сообщений Наконец, добавьте метод BatchLoad, выберите тип возвращаемого значения long и укажите один параметр FileName типа LPCTSTR. После этого элемент можно будет откомпилировать и запустить, но присутствие новых методов (кроме Refresh) никак не скажется на его работе. Чтобы новые методы заработали, необходимо сделать следующее: 1.
Во время создания элемента открыть индексный файл (HRESULTS.IND), загрузить его содержимое в память и закрыть файл. 2. Во время уничтожения элемента освободить ресурсы, использованные для хранения индексного файла в памяти. 3. Изменить функцию SetHResult, чтобы она получала информацию о HRESULT из индексного файла в памяти и файла сообщений (HRESULTS.MSG). 4. Написать код методов Add и BatchLoad. Вроде бы пока ничего сложного. Чтобы дополнительно облегчить задачу, мы начнем с определения структуры данных, которая будет использоваться для хранения содержимого индексного файла. Для начала определим в файле FIRSTCTL.H структуру CHRESULTEntry, которая будет использоваться для хранения пар HRESULT/смещение:
www.books-shop.com
struct CHRESULTEntry { long lHRESULT; unsigned long ulOffset; };
ДЛЯ ВАШЕГО СВЕДЕНИЯ А вы знаете, что в C++ ключевые слова class и struct считаются синонимами? Единственное отличие заключается в том, что по умолчанию члены структуры считаются открытыми (для обеспечения совместимости со структурами C), а члены класса — закрытыми.
Структура содержит два поля — длинное целое для хранения HRESULT и длинное целое без знака для хранения смещения информации о нем в файле сообщений. Мы будем работать с массивом структур CHRESULTEntry, размер которого, разумеется, должен определяться динамически, поскольку во время компиляции мы еще не знаем, сколько HRESULT содержится в индексном файле; к тому же пользователь может в любой момент добавить новые сведения. Соответственно, мы объявляем закрытый указатель в классе CFirstCtrl:
CHRESULTEntry *m_lpseCodes; В момент создания элемента указатель нужно инициализировать так, чтобы разместить в памяти все записи индексного файла. Новые HRESULT, добавленные методом Add, заносятся в связанный список, поддерживаемый элементом, добавленные методом BatchLoad попадают непосредственно в индексный файл. В конце загрузки такой файл считывается заново, а массив снова размещается в памяти. В файл FIRSTCTL.H необходимо добавить класс для работы со связанным списком:
class CHRESULTEntryList { public: void SetNext(CHRESULTEntryList *selNew) { m_pNext = selNew; } CHRESULTEntryList *GetNext(void) const { return m_pNext }; CHRESULTEntry *GetEntry(void) { return &m_seThis; } private: CHRESULTEntryList *m_pNext; CHRESULTEntry m_seThis; }; Переменная m_pNext содержит указатель на следующий элемент списка, а m_seThis — значение HRESULT для текущего элемента. Функция SetNext заносит в m_pNext переданный указатель на структуру; обычно она используется для пополнения списка — переменная m_pNext текущей последней структуры начинает указывать на новую структуру, оказавшуюся последней. Функция GetNext используется для перемещения по списку; она возвращает указатель на следующий элемент списка, а GetEntry возвращает указатель на объект CHRESULTEntry для текущего элемента списка. Кроме того, нам понадобятся еще две закрытых переменных класса CFirstCtrl: m_lpseNewCodes и m_lpseListEnd. Первая указывает на начало списка новых элементов, а вторая — на его конец. Указатель на конец списка хранится лишь для того, чтобы ускорить добавление элементов в конец списка. В листинге 7-1 приведен код конструктора и деструктора, который обеспечивает чтение индексного файла в память и ее освобождение при уничтожении элемента. Контейнер обнуляет указатели и переменную m_lHRESULTs, в которой будет храниться количество записей в прочитанном индексном файле. Затем контейнер вызывает ReadIndexFile —новую функцию, которая загружает индексный файл в память. Эта функция будет вызываться и после вызова BatchLoad для повторной загрузки файла в память. Деструктор освобождает всю память,
www.books-shop.com
занимаемую индексным файлом и списком новых значений, отдельной функцией ClearList. Наличие такой функции облегчает повторное использование этого фрагмента в будущем. Следует учесть одну досадную мелочь из области C++ — разумеется, вызов ReadIndexFile может закончиться неудачей (скажем, если функция не найдет индексный файл). Если же неудачный вызов функции будет произведен из конструктора, становится непонятно, что же ему делать дальше. Он не может возбудить исключение, потому что создатель не будет знать, что ему делать с частично созданным объектом. Он также не может вернуть код ошибки. Соответственно, в случае неудачи ReadIndexFile просто выводит отладочное сообщение, однако элемент при этом теряет все полезные возможности. Листинг 7-1. Конструктор и деструктор элемента с функцией чтения индексного файла в память
uRead1 = cfIndex.Read(&lCode, sizeof(lCode)); uRead2 = cfIndex.Read(&ulOffset, sizeof(ulOffset)); if (uRead1 == 0 && uRead2 == 0) { break; } if ((uRead1 == sizeof(lCode)) && (uRead2 == sizeof(ulOffset))) { m_lpseCodes[lCurrent].lHRESULT = lCode; m_lpseCodes[lCurrent].ulOffset = ulOffset; ++lCurrent; } else { AfxThrowFileException(CFileException::endOfFile); } } while (uRead1); cfIndex.Close(); } catch (CException *e) { TRACE(_T("Error reading index file or out of memory\n")); delete [] m_lpseCodes; m_lpseCodes = 0; m_lHRESULTs = 0; e -> Delete(); } } else { TRACE(_T("Index file not found — will be created\n")); } } В функции ReadIndexFile переменная csIndex типа CString используется для загрузки имени индексного файла из строковой таблицы (я занес в таблицу строку C:\\CONTROLS\\CHAP07\\FIRST\\HRESULTS.IND, но вы, разумеется, можете использовать любое другое значение). Затем функция открывает файл для чтения при помощи переменной cfIndex класса CFile. Если попытка окажется неудачной, ReadIndexFile просто выводит информационное сообщение на отладочный терминал. Если же файл открывается успешно, выполняется основное тело функции. Обратите внимание на то, что оно заключено внутри одного громадного try-блока для того, чтобы перехват и обработка ошибок производились единообразно. Сначала функция определяет количество записей в индексном файле делением его длины на размер одной записи. Размер записи хранится в виде константы, он равен сумме размера длинного целого (HRESULT) и длинного целого без знака (смещения). В среде Win32 это составит 8 байт. Количество записей сохраняется в m_lHRESULTs. Переменной m_lpseCodes присваивается указатель на динамически размещаемый массив из m_lpseCodes структур CHRESULTEntry. При неудачной попытке выделения памяти реализация оператора new библиотеки MFC возбуждает исключение CMemoryException. Затем функция входит в цикл, в котором она читает пары HRESULT/смещение в соответствующие поля каждой структуры массива. Цикл прекращается при достижении конца файла или инициировании любого файлового исключения. Обратите внимание на то, что при неожиданном достижении конца файла (при котором оказывается прочитанным менее 8 байт) инициируется исключение AfxThrowFileException, перехватываемое несколькими строками ниже. Все исключения перехватываются в catch-блоке, в котором вместо класса, соответствующего категории исключений, указан базовый класс CException. Это сделано потому, что любые исключения в нашем случае обрабатываются одинаково — удаляется память, выделенная под массив. Возможно, обработчик исключения будет вызван до распределения памяти, в этом
www.books-shop.com
случае переменная m_lpseCodes будет иметь значение 0, присвоенное ей в конструкторе. Наш код упрощается благодаря тому обстоятельству, что оператору delete в C++ можно передать указатель null — в этом случае delete просто завершается, не выполняя никаких действий. В листинге 7-2 приведен код, который используется для присвоения нового значения свойству HResult. Функция SetHResult выглядит проще своей предыдущей версии. Она инициализирует строки сообщений и символического имени, после чего вызывает новую функцию FindEntry для поиска HRESULT в индексном файле, размещенном в памяти. Если поиск окажется успешным, FindEntry заносит во второй параметр смещение внутри файла сообщений, по которому может быть найдена запись для данного HRESULT. Значение смещения используется новой функцией GetInfo для заполнения строк сообщений и символического имени информацией из файла сообщений. Как и в предыдущей версии, при неудачном поиске HRESULT или возникновении ошибки мы присваиваем свойству Caption специальную строку ошибки. В функции FindEntry нет ничего сложного. Она сначала просматривает массив записей, прочитанных из индексного файла, и, если ей не удается найти среди них нужный HRESULT, просматривает связанный список добавленных элементов. Разумеется, эту процедуру можно оптимизировать — отсортировать индексный файл (или по крайней мере его образ в памяти) по HRESULT, что позволит заменить линейный поиск более эффективным бинарным. Это несколько улучшит производительность, но усложнит некоторые фрагменты кода — например, чтение несортированного файла или добавление в него новых записей. GetInfo выглядит посложнее. Она открывает файл сообщений (идентифицируемый новым строковым ресурсом IDS_MESSAGEFILE) для чтения и переходит к заданному смещению. Затем она читает символы до тех пор, пока не будет найден 0, и заносит их в строку символьного имени. Обратите внимание на усложнение кода, обусловленное Unicode, — в старые времена ANSI-кодировки такой код было бы проще написать и проще разобраться в нем, однако нам пришлось бы полностью отказаться от обработки информации на некоторых языках (вообще-то сообщения об ошибках в WINERROR.H не локализованы, но корректное программирование — дело принципа). После чтения символического имени операция повторяется для строки сообщения, после чего файл закрывается. Листинг 7-2. Чтение информации о HRESULT
} Теперь можно рассмотреть код самого метода Add, приведенный в листинге 7-3. Сначала вызывается FindEntry, чтобы проверить, присутствует ли в файле добавляемый HRESULT. Если он действительно существует, функция прекращает работу с кодом успешного завершения. Сравниваются только значения HRESULT, проверять строки сообщений и символического имени не нужно (при желании можете добавить эту проверку самостоятельно). Если HRESULT не найден в файле, он заносится в связанный список новых записей. Далее мы выделяем память под новый объект CHRESULTEntryList и инициализируем его внутренний объект CHRESULTEntry при помощи функции GetEntry. Смещение инициализируется величиной, возвращаемой AddMessage (см. ниже), после чего величина смещения и HRESULT заносятся в индексный файл функцией WriteEntry (также см. ниже). Чтобы внести новую запись в список, мы изменяем указатель текущей последней записи так, чтобы он ссылался на новую запись, и затем делаем то же самое с переменной m_lpseListEnd (последняя запись в списке). Если добавляемая запись оказывается первой в списке, нужно также присвоить указатель на нее переменной m_lpseNewCodes, указывающей на начало списка. Листинг 7-3. Метод Add и вспомогательные функции
Функция AddMessage добавляет новое символическое имя и сообщение в файл сообщений. Для этого мы открываем файл для записи и производим фиктивный «поиск», чтобы перевести текущую позицию в конец файла, где будут добавляться новые данные. Затем мы сохраняем этот адрес (текущую длину файла), которая будет возвращаться функцией, и последовательно записываем в файл сообщений символьное имя, нуль-терминатор, сообщение и еще один нультерминатор. Обратите внимание на функцию _tclen, которая преобразуется в соответствующую функцию для определения длины строки в зависимости от того, для какой кодировки компилируется элемент — Unicode или ANSI. Функция WriteEntry записывает HRESULT и смещение информации внутри файла сообщения в индексный файл. Она открывает индексный файл для записи, перемещается в конец и записывает два длинных целых из полученной структуры CHRESULTEntry. Наконец давайте рассмотрим метод BatchLoad, приведенный в листинге 7-4. В нем используется следующий алгоритм: 1.
Открыть входной файл для чтения в текстовом режиме.
www.books-shop.com
2. Открыть индексный файл для записи; перейти в конец. 3. Открыть файл сообщений для записи. 4. Выполнять следующее, пока не будет достигнут конец файла:
Получить из входного файла HRESULT, символьное имя и текст сообщения. Проверить, существует ли HRESULT в индексном файле; если существует, пропустить дальнейшие действия. Перейти в конец файла сообщений и получить текущую позицию. Записать HRESULT и смещение внутри файла сообщений в индексный файл. Записать символическое имя и сообщение в файл сообщений.
5. Продолжать цикл. 6. Закрыть все файлы. 7. Удалить связанный список добавлений из метода Add. 8. Удалить область памяти, в которой хранится образ индексного файла. 9. Заново загрузить индексный файл. Листинг 7-4. Метод BatchLoad и вспомогательные функции
long CFirstCtrl::BatchLoad(LPCTSTR FileName) { CFile cfIndex, cfMsg; CStdioFile cfInput; long lEntries = 0; if (cfInput.Open(FileName, CFile::typeText | CFile::modeRead) == TRUE) { CString csIndex; try { csIndex.LoadString(IDS_INDEXFILE); if (cfIndex.Open(csIndex, CFile::modeWrite | CFile::shareExclusive) == TRUE) { CString csMsg; csMsg.LoadString(IDS_MESSAGEFILE); if (cfMsg.Open(csMsg, CFile::modeWrite | CFile::shareExclusive) == TRUE) { lEntries = DoBatchLoad(&cfInput, &cfIndex, &cfMsg); cfMsg.Close(); } else { TRACE(_T("Failed to open message file\n")); } cfIndex.Close();
} else { TRACE(_T("Failed to open index file\n")); } cfInput.Close(); } catch (CException *e) { TRACE(_T("Error closing files\n")); e -> Delete(); }
} else
www.books-shop.com
{ }
TRACE(_T("Failed to open input file\n"));
if (lEntries) { ClearList(); m_lpseCodes = 0; m_lpseNewCodes = 0; m_lpseListEnd = 0; m_lHRESULTs = 0; ReadIndexFile(); } return lEntries; } long CFirstCtrl::DoBatchLoad(CStdioFile *cfIn, CFile *cfIndex, CFile *cfMsg) { long lEntries = 0; try { cfIndex -> Seek(0, CFile::end); CString csLine, csMsg, csSymbol; while (GetNextDefineLine(cfIn, &csLine, &csMsg)) { long lCode = GetTheCode(&csLine, &csSymbol); unsigned long ulOffset; if (FindEntry(lCode, &ulOffset)) { TRACE1(_T("HRESULT %08X already in database ignored\n"), lCode); } else { long lMsgPos = cfMsg -> Seek(0, CFile::end); cfIndex -> Write(&lCode, sizeof(lCode)); cfIndex -> Write(&lMsgPos, sizeof(lMsgPos));
long CFirstCtrl::GetTheCode(CString *csLine, CString *csSymbol) { // Пропустить ‘#define’ int i = 7; // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { return 0; } // Получить символическое имя csSymbol -> Empty(); while ((csLine -> GetLength() > i) && !(_istspace(csLine -> GetAt(i)))) { *csSymbol += csLine -> GetAt(i); ++i; } if (csLine -> GetLength() <= i) { csSymbol -> Empty(); return 0; } // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { csSymbol -> Empty(); return 0; } // В последних версиях файла WINERROR.H номер ошибки // может быть представлен в виде _HRESULT_TYPEDEF(номер), // в этом случае следует пропустить имя макроса. int pos; if ((pos = csLine -> Find(_T("_HRESULT_TYPEDEF_("))) != -1) { i = pos + 18; // Длина имени макроса равна 18 } // Получить номер CString csNumber; try { csNumber = csLine -> Mid(i); }
www.books-shop.com
catch (CMemoryException *e) { csSymbol -> Empty(); e -> Delete(); return 0; } return _tcstoul(csNumber, NULL, 0); } Работа функции BatchLoad начинается с обнуления счетчика записей. Затем мы открываем три файла. Входной файл открывается через объект класса CStdioFile, а не CFile, поскольку нам снова потребуется воспользоваться специальными возможностями этого класса по обработке текста. Затем BatchLoad вызывает функцию DoBatchLoad, которая выполняет большую часть работы и возвращает количество записей, внесенных в базу данных. Если операции закрытия всех файлов прошли успешно, BatchLoad стирает из памяти образ старого индексного файла, удаляет записи из связанного списка, созданного методом Add, и затем заново загружает индексный файл функцией ReadIndexFile. Большая часть кода DoBatchLoad расположена во всеобъемлющем блоке try-catch, поскольку некоторые из выполняющихся в них операций способны возбудить исключения. Внутри этого блока мы переходим к концу индексного файла, после чего начинаем в цикле вызывать GetNextDefineLine, пока не будет найден конец входного файла или не возникнет какая-нибудь ошибка. GetNextDefineLine представляет собой разновидность одноименной функции, с которой мы работали раньше, за исключением того, что объект сообщения CString теперь передается в качестве параметра, тогда как ранее для этого использовалась переменная m_csMessage. Когда функция обнаруживает строки, соответствующие критериям поиска HRESULT и строк сообщения, она читает сообщение, после чего продолжает читать до строки, содержащей символическое имя и значение HRESULT (включая данную строку). Еще одна функция, взятая в слегка измененной форме из предыдущих глав — GetTheCode, которой теперь объект символического имени CString передается в качестве параметра (вместо использования переменной m_csSymbol). Она извлекает символическое имя из переданной ей строки, а также получает значение HRESULT. Затем DoBatchLoad при помощи FindEntry определяет, совпадает ли прочитанное значение HRESULT с каким-либо из присутствующих в базе. Если совпадающее значение будет найдено, мы пропускаем эту запись и читаем следующую строку; в противном случае DoBatchLoad переходит в конец файла сообщений и записывает в индексный файл значения HRESULT и текущего смещения внутри файла сообщений. Затем мы записываем в файл сообщений символическое имя и строку сообщения, после чего увеличиваем счетчик записей. Окончательный вариант элемента из этой главы находится на прилагаемом диске CD-ROM в каталоге \CODE\CHAP07\FIRST. В этом же каталоге имеются файлы HRESULTS.IND и HRESULTS.MSG, созданные на основе файла WINERROR.H, входящего в Visual C++ 4.2, при помощи метода BatchLoad. BatchLoad сообщает, что в этом файле определено 1013 значений HRESULT. Я также прилагаю файлы EMPTY.IND и EMPTY.MSG, которые могут пригодиться при тестировании. В этих файлах содержится всего одно значение HRESULT — 0x00000000, что делает их удобной отправной точкой для тестирования метода BatchLoad. Чтобы файл был опознан методом BatchLoad, он должен иметь тот же формат, что и WINERROR.H; другими словами, он должен выглядеть так, словно был сгенерирован утилитой Message Compiler (MC). Загрузив полный набор HRESULT, вы наверняка заметите, что эта версия элемента работает заметно быстрее предыдущей.
7.5 Ошибки и исключения В новом коде я достаточно снисходительно отнесся к возможным ошибкам и исключениям. Тем не менее в ряде мест я попытался обработать ошибки, маловероятные в 32-разрядной системе — например, неудачные попытки выделения маленьких областей памяти. Вдобавок от этого программа стала «читабельной». Я возвращаю значение, свидетельствующее о возникновении ошибки, но при этом не указываю тип ошибки, в других случаях ошибки попросту игнорируются. Ситуацию необходимо исправить, но перед этим нужно познакомиться с двумя дополнительными аспектами работы элементов: событиями и исключениями. Мы рассмотрим их в двух ближайших
www.books-shop.com
главах, а потом воспользуемся полученными сведениями и сделаем наш элемент более функциональным и удобным.
7.6 Добавление методов в элементы, написанные без использования MFC Все это, конечно, замечательно, если вы создаете элементы на базе MFC. Если же вы не пользуетесь этой библиотекой (однако пишете свои элементы на C++), добавление методов будет происходить аналогично добавлению свойств, за исключением того, что для свойств чаще всего приходится реализовывать две функции доступа (по одной для чтения и записи), а для метода хватает одной. Конечно, метод элемента представляет собой обычный метод Automation, так что рассмотренный ранее синтаксис работает для элементов точно так же, как и для любого объекта Automation. Как и при добавлении свойств, необходимо проследить за тем, чтобы описание интерфейса в IDL-файле элемента (или ODL-файле) совпадало с его фактической реализацией, а скомпилированная библиотека типов содержала точное описание интерфейса. Если вы внимательно следите за изложением материала и при этом не работаете с MFC, возможно, вам захочется переписать элемент First на свой лад. Так, используемый мной класс CString можно заменить другим строковым классом — например, тем, который входит в предложенный ANSI стандарт C++. С файловыми операциями хлопот будет побольше, но читателю следует уяснить, что конкретная реализация свойств и методов элемента не имеет особого значения для этой книги — для нее гораздо важнее то обстоятельство, что у элемента есть свойства и методы.
www.books-shop.com
Глава
8
События События — одна из самых интересных новинок, появившихся в спецификации Элементов ActiveX. События расширяют круг возможностей элемента. В этой модели вы определяете интерфейс и сами взаимодействуете с ним; сравните с прежней ситуацией, когда вы определяли (и реализовывали) интерфейс, с которым могли взаимодействовать другие. Как показано в главе 3, поддержка событий в Элементах ActiveX представляет собой универсальный механизм COM, который может применяться любыми объектами, желающими работать с интерфейсами других объектов. Когда я только начал писать эту книгу, ценность событий казалась сомнительной, поскольку в той реализации COM отсутствовала возможность маршалинга для интерфейсов точек соединения (например, IConnectionPoint), поэтому они могли использоваться только внутри процесса. Тем не менее поддержка маршалинга этих интерфейсов была включена в 32-разрядный COM Microsoft Windows NT 4.0 и Windows 95, где она была впервые использована в Microsoft Internet Explorer 3.0. Чтобы познакомиться с событиями в элементах ActiveX и MFC, мы рассмотрим их с точки зрении спецификации, а затем разберем, какие возможности для работы с ними предусмотрены в MFC и Microsoft Visual C++. Кроме того, я покажу, как включить события в элемент без помощи MFC, и добавлю пару событий в элемент First, над которым мы работаем в последних главах.
8.1 Возможные применения событий События отлично дополняют арсенал программиста. Например, стандартные объекты Automation могли сообщать своему контейнеру о том, что произошло что-то интересное, лишь в результате обращения к свойствам или методамобъекта, то есть синхронно. Элементы ActiveX могут общаться с контейнером в любой момент, асинхронно и независимо от каких-либо действий со стороны контейнера. Я должен пояснить, что имеется в виду под «синхронностью» и «асинхронностью». В данном случае речь идет о взаимодействии объекта с контейнером, а не об асинхронности операции по отношению к самому объекту. Другими словами, элемент может в любой момент сообщить контейнеру о наступлении какого-то события, но это не происходит асинхронно к основной работе объекта. После вызова функции, которая сообщает контейнеру о наступлении события, элемент должен подождать ее успешного завершения и лишь затем продолжать свою работу — впрочем, это справедливо лишь отчасти. В предыдущем издании книги я почти игнорировал работу с потоками внутри элемента, поскольку существовавшая реализация MFC создавала только однопоточные элементы, все коммерческие версии контейнеров также были однопоточными, а книга ориентировалась на 16-разрядное программирование. С тех пор все изменилось. MFC 4.2 (и более поздних версий) позволяет создавать элементы с совместной потоковой моделью, необходимо лишь выполнять основные правила. Разумеется, при помощи ATL или Microsoft Visual J++ вы также сможете создавать элементы со свободной потоковой моделью или поддерживать две модели одновременно. Так как же эти новые возможности влияют на работу элементов? Обычно никак, поскольку большинство контейнеров все еще пишется в расчете на то, что события будут инициироваться из того потока, в котором они были созданы. С другой стороны, элементы все чаще применяются в Web-броузерах, для которых однопоточной модели оказывается недостаточно. Так или иначе, теперь элемент может послать событие своему контейнеру из другого потока, так что иногда события можно считать асинхронными даже в том смысле, в котором (по моим же словам, приведенным выше) они асинхронными не являются!
Для чего используются события? С их помощью элемент может уведомить контейнер о любой ситуации, которая может представлять для него интерес. Очевидными кандидатами являются ошибки (например, разрыв сетевого соединения), сообщения о том, что текущая запись базы данных редактируется кем-то другим, и т. д. Важно понимать, что даже при наличии этих очевидных кандидатов разработчик элемента может создавать любые события, которые будут сообщать контейнеру о чем угодно. Исключения и ошибки будут рассматриваться в следующей главе, а пока мы будем заниматься чисто информационными событиями.
8.2.1 Request-события Элемент инициирует request-событие, чтобы запросить у контейнера разрешение выполнить ту или иную операцию. Последним параметром таких событий является указатель на переменную (обычно она называется Cancel) типа CancelBoolean — это стандартный тип, определенный в спецификации Элементов ActiveX. Перед инициированием событию переменной, на которую ссылается указатель, присваивается значение FALSE. Если контейнер заменяет его на TRUE, значит, он не хочет, чтобы элемент выполнял операцию, связанную с данным событием. Таким событиям следует присваивать имена, начинающиеся с Request, — например, событие для запроса на обновление базы данных может называться RequestUpdate.
8.2.2 Before-события Элемент возбуждает before-событие перед тем, как выполнять ту или иную операцию, — это позволяет контейнеру или пользователю элемента подготовиться к происходящему. Например, элемент может возбудить событие BeforeClose непосредственно перед завершением своей работы, тем самым элемент позволяет контейнеру сделать все, что тот сочтет нужным, перед тем как прекратить свое существование. Before-события не могут быть отменены. Им следует присваивать имена вида Beforexxx, где xxx — действие, предпринимаемое элементом.
8.2.3 After-события Элемент возбуждает after-событие, чтобы контейнер или пользователь элемента мог что-то сделать после того, как элемент выполнит некоторую операцию. After-события представляют собой классические уведомляющие сообщения; это самый распространенный тип событий, поэтому имена after-событий не подчиняются никаким особым конвенциям. Такие события не могут быть отменены. Типичным примером служит событие, при помощи которого элемент сообщает контейнеру о щелчке мышью.
8.2.4 Do-события Do-события чем-то похожи на виртуальные функции C++ — пользуясь ими, контейнер или пользователь может изменить операцию или сделать что-то перед выполнением действий по умолчанию. Если для do-события предусмотрены действия по умолчанию, то перед его возбуждением элемент присваивает значение TRUE логической переменной, которая обычно называется EnableDefault (указатель на нее является последним параметром события). Если контейнер заменяет ее значение на FALSE, значит, он просит элемент не выполнять действия по умолчанию. Do-события обычно получают имена Doxxx, где xxx — выполняемая операция.
8.3 Инициирование событий
www.books-shop.com
В текущей реализации элемент инициирует событие, вызывая метод Invoke через указатель на интерфейс IDispatch (интерфейс диспетчеризации), полученный им от контейнера в тот момент, когда контейнер подключается к его точке соединения методом IConnectionPoint::Advise. Поскольку точки соединений могут быть мультиплексными, это может привести к вызову сразу нескольких обработчиков события; другими словами, событие будет получено всеми приемниками, подключенными к данной точке. Элемент сам определяет интерфейс события, и все, что от него требуется — упаковать параметры (если они имеются) в переменные VARIANT и вызвать Invoke. Конечно, существует теоретическая возможность того, что определяемый элементом интерфейс событий является двойственным, следовательно, при подключении к точке соединения элемента контейнер должен создать реализацию этого двойственного интерфейса. Тем не менее это вызывает некоторые затруднения: как элементу узнать, создал ли контейнер двойственный интерфейс (и, следовательно, к нему можно обращаться через v-таблицу), или же он реализовал лишь его часть для интерфейса IDispatch, вследствие чего к нему нужно обращаться через Invoke? Все известные мне коммерческие контейнеры ожидают, что предназначенные для них события будут возбуждаться через IDispatch. В их число входит и язык Visual Basic Scripting. Тем не менее разработчик элементов на C++ может воспользоваться библиотекой ATL, которая позволяет создать точку соединения для двойственного интерфейса. На самом деле текущая реализация ATL в этом случае заставляет разработчика создать сразу две точки соединения — одну для двойственного интерфейса, обращения к которой всегда производятся через v-таблицу, а другую для эквивалентного интерфейса диспетчеризации dispinterface (для которого определяется отдельный GUID), которая вызывается через Invoke.
8.4 Стандартные события Спецификация Элементов ActiveX определяет некоторые стандартные события, которые могут возбуждаться элементом. Сказанное не означает, что каждый элемент должен уметь возбуждать каждое из этих событий — скорее это говорит о том, что для самых распространенных событий определена стандартная семантика. Стандартные события, определенные в спецификации Элементов ActiveX, перечислены в приведенной ниже таблице. Нестандартные события (те, которые вы определяете самостоятельно) должны иметь положительные значения dispid, чтобы они не конфликтовали со стандартными событиями, методами или свойствами. Хотя в спецификации Элементов ActiveX не определено ни одно расширенное событие, контейнеры могут предоставлять такие события своим элементам (по аналогии с расширенными свойствами и методами). Например, Visual Basic 4.0 добавляет в перечень стандартных событий элемента следующие события:
DragDrop DragOver GotFocus LostFocus
Эти события, как и другие расширенные атрибуты, не реализуются самим элементом.
8.4.1 Стандартные события (по спецификации Элементов ActiveX) Имя
Dispid
Описание
Click
–600
Событие обычно инициируется при щелчке элемента мышью. Некоторые элементы могут быть запрограммированы так, чтобы инициировать его при изменении значений определенных свойств.
DblClick
–601
Событие инициируется при двойном щелчке элемента.
KeyDown
–602
Событие инициируется, когда при наличии фокуса у элемента нажимается клавиша.
KeyUp
–604
Событие инициируется, когда при наличии фокуса у элемента отпускается клавиша.
www.books-shop.com
MouseDown
–605
Событие инициируется, когда пользователь нажимает кнопку мыши на элементе.
MouseMove
–606
Событие инициируется, когда пользователь перемещает курсор мыши над элементом.
MouseUp
–607
Событие инициируется, когда пользователь отпускает кнопку мыши на элементе.
–608
Событие инициируется элементом, когда он хочет сообщить контейнеру о произошедшей ошибке. В классическом варианте такие сообщения происходили синхронно с вызовом метода или обращением к свойству (событие позволяет элементу асинхронно сообщить контейнеру об ошибке; эта тема более подробно рассматривается в следующей главе).
–609
Событие используется во время загрузки асинхронных (путевых) свойств. Когда количество данных, полученных элементом, заставляет его перейти в следующее состояние готовности, инициируется данное событие. О нем, как и об асинхронных событиях вообще, рассказано в главе 13, «Элементы ActiveX и Internet».
Error
ReadyStateChange
8.5 События, MFC и Visual C++ Конечно, в MFC и Visual C++ предусмотрены средства для добавления событий в элементы ActiveX. Как обычно, для этого в основном используется ClassWizard и его вкладка OLE Events. Если у выделенного класса имеется схема событий (аналог схемы диспетчеризации, но для событий), на вкладке OLE Events становится доступной кнопка Add Event, при помощи которой добавляются и стандартные и нестандартные события. Все события из приведенной выше таблицы MFC рассматривает как стандартные, все прочие события считаются нестандартными. Реализация стандартных событий в MFC полностью соответствует спецификации Элементов ActiveX, так что вы можете не сомневаться в ее правильности. На рис. 8-1 изображено стандартное событие, добавляемое в проект элемента в диалоговом окне Add Event.
Рис.8-1.Диалоговое окно Add Event в Microsoft Visual C++ версии 2.0 и выше используется для добавления стандартных событий в проект элемента
8.6 Добавление стандартного события Когда вы добавляете в проект стандартное событие (например, Click), ClassWizard добавляет в схему событий класса, находящуюся в файле реализации, макрос вида
www.books-shop.com
EVENT_STOCK_CLICK() Теперь вы можете в любом месте своего кода вызвать функцию FireClick, чтобы инициировать событие Click и отправить его контейнеру элемента. Функция FireClick, принадлежащая классу COleControl, через вспомогательную функцию вызывает метод Invoke интерфейса (-ов) IDispatch, подключенного (-ых) к точке соединения, которая служит для инициирования событий. Если к точке соединения не подключен ни один интерфейс, событие не инициируется.
8.7 Добавление нестандартного события При добавлении в проект нестандартного события при помощи ClassWizard в схему событий заголовочного файла заносится функция Firexxx, где xxx — имя события (вообще говоря, имя функции можно изменить, но зачем?) Так, для события InvalidHResult, имеющего один параметр (длинным целым, значение которого соответствует HRESULT), будут добавлены следующие строки:
void FireInvalidHResult(long HResult) {FireEvent(eventidInvalidHResult,EVENT_PARAM(VTS_I4), HResult);} В этом фрагменте определяется функция FireInvalidHResult, которая получает один параметр и не возвращает никакого результата. Вызов события вообще не может возвращать результата. Функция FireEvent, которая встречается во всех функциях инициирования событий (в том числе и стандартных), реализуется ClassWizard. Она принадлежит классу COleControl и получает переменное количество параметров, поскольку это универсальная функция для инициирования любого события. Функция FireEvent вызывает FireEventV — еще одну вспомогательную функцию, которая просматривает список всех интерфейсов, подключенных к текущей точке соединения, и затем обращается к функции COleDispatchDriver::InvokeHelperV, чтобы вызвать Invoke для каждого из них. Класс COleDispatchDriver используется библиотекой MFC для создания клиентской прослойки объектов Automation на C++ и работы с ней. Другими словами, COleDispatchDriver используется для вызова методов и свойств через реализацию IDispatch другого объекта. Как видите, библиотека MFC пользуется собственным механизмом для работы с атрибутами объектов. Исходный текст всех функций-обработчиков событий класса COleControl находится в файле CTLEVENT.CPP исходных текстов MFC. При добавлении нестандартного события ClassWizard также заносит в схему событий в файле реализации макрос следующего вида:
EVENT_CUSTOM("InvalidHResult", FireInvalidHResult, VTS_I4) Данный макрос определяет имя события, инициирующую его функцию и параметры. После того как функция инициирования события будет внесена в схему событий, вы можете в любой момент вызвать ее — это приведет к инициированию события элементом. Тем не менее нет никакой гарантии, что контейнер всегда готов обработать полученные события. Например, он может вызвать метод IOleControl::FreezeEvents, чтобы запретить элементу инициировать события. Для элементов на базе MFC это приводит к вызову функции OnFreezeEvents. Реализация этой функции из COleControl не делает ничего, но вы можете переопределить ее поведение так, чтобы сохранять все события до момента разблокировки событий, или установить на время блокировки флаг, запрещающий инициирование событий. Более интересный случай рассматривается в следующем разделе, где мы добавим в элемент First несколько нестандартных событий.
8.8 Добавление нестандартных событий в элемент First Текущий вариант элемента First, разработанный нами на протяжении последних глав, обладает двумя недостатками. Во-первых, он не умеет создавать файлы (индексный и файл сообщений), если они не существуют. Во-вторых, он не может на программном уровне сообщить вам о том, что свойству HResult было присвоено недопустимое значение HRESULT. Конечно, со вторым недостатком можно справиться — присваивая значение одному свойству, заодно проверить другое на допустимость, но такой вариант выглядит неестественно. Согласитесь, мы имеем дело с классическим случаем для применения события — элемент сообщает контейнеру о том, что при выполнении последней операции произошла ошибка (как мы
www.books-shop.com
увидим в следующей главе, то же самое можно сделать лучше, чем мы сделаем сейчас, но дело не в этом…). Чтобы исправить оба недостатка нашего элемента, мы добавим в него: Код, который создает несуществующий индексный файл и файл сообщения, а также инициирует событие для контейнера при успешном создании файлов. Событие, которое будет сообщать контейнеру о том, что свойству HResult было присвоено недопустимое значение HRESULT. Значение считается недопустимым, если оно отсутствует в текущем представлении индексного файла в памяти.
Мы добавим два нестандартных события: InvalidHResult и FilesCreated. Первое из них получает один параметр — длинное целое с именем HResult, а второе обходится без параметров. Добавьте эти события при помощи ClassWizard и убедитесь, что схема событий в заголовочном файле выглядит так:
// Схемы событий //{{AFX_EVENT(CFirstCtrl) void FireInvalidHResult(long HResult) {FireEvent(eventidInvalidHResult,EVENT_PARAM(VTS_I4), HResult);} void FireFilesCreated() {FireEvent(eventidFilesCreated,EVENT_PARAM(VTS_NONE));} //}}AFX_EVENT DECLARE_EVENT_MAP() Затем проверьте схему событий в файле реализации: BEGIN_EVENT_MAP(CFirstCtrl, COleControl) //{{AFX_EVENT_MAP(CFirstCtrl) EVENT_CUSTOM("InvalidHResult", FireInvalidHResult, VTS_I4) EVENT_CUSTOM("FilesCreated", FireFilesCreated, VTS_NONE) //}}AFX_EVENT_MAP END_EVENT_MAP()
Если схемы событий выглядят нормально, значит, функции событий существуют и вы можете их вызвать. Если заглянуть в ODL-файл, можно заметить, что в интерфейсе событий теперь присутствуют два новых описания:
//
Интерфейс диспетчеризации событий для CFirstCtrl
[ uuid(A29DB7D4-E4E5-11CF-848A-00AA005754FD), helpstring("Event interface for First Control") ] dispinterface _DFirstEvents { properties: // Интерфейс событий не имеет свойств
};
methods: // ВНИМАНИЕ - здесь ClassWizard будет хранить // информацию о событиях. // Соблюдайте крайнюю осторожность // при редактировании этой секции. //{{AFX_ODL_EVENT(CFirstCtrl) [id(1)] void InvalidHResult(long HResult); [id(2)] void FilesCreated(); //}}AFX_ODL_EVENT
Обновленный вариант показывает, что к точке соединения элемента, через которую передаются события, будет подключена реализация dispinterface с именем _DFirstEvents и двумя методами; dispid первого метода равен 1, а второго — 2. Для того чтобы инициировать событие в случае присвоения HResult недопустимого значения, достаточно добавить в функцию SetHResult всего одну строку. Она вызывает функцию инициирования события FireInvalidHResult и передает недопустимое значение HResult:
www.books-shop.com
FireInvalidHResult(nNewValue); Эта строка должна находиться сразу же после строки
сsLine.LoadString(IDS_NOVALID_HRESULT); в условии else оператора if. Если теперь присвоить свойству HResult значение, отсутствующее в индексном файле (или при возникновении любой другой ошибки), элемент направляет контейнеру событие InvalidHResult. Попробуйте скомпилировать новую версию элемента и протестировать ее. Со второй частью (созданием индексного файла и файла сообщений) дело, конечно, обстоит посложнее. В программе существует всего один логичный момент для создания этих файлов — если при первой попытке чтения выясняется, что таких файлов нет в указанном месте. Это происходит на ранней стадии жизненного цикла элемента — в последнем операторе конструктора CFirstControl, при вызове ReadIndexFile. Посмотрите на функцию ReadIndexFile; ее работа начинается с попытки открыть индексный файл для чтения. Эта попытка выполняется внутри оператора if, поэтому при неудаче можно предпринять какие-либо действия. Пока мы ограничиваемся выдачей строки на отладочный терминал с завершением функции, однако пользы от такой «обработки» немного. Функцию необходимо изменить, чтобы она сначала могла проверить, существуют ли эти файлы (на самом деле она будет проверять только индексный файл), а если не существуют — создать их. В классе CFile имеется полезная функция GetStatus, предназначенная именно для этой цели. GetStatus существует в двух вариантах, один из которых выполняется для открытого объекта CFile и возвращает информацию о нем. Другая версия — статическая, она получает имя файла, ищет файл с таким именем и возвращает информацию о нем. Мы воспользуемся вторым вариантом, поскольку у нас пока еще нет открытого объекта CFile. Статическая функция CFile не обязана (на самом деле — и не может) выполняться для существующего экземпляра CFile, поэтому мы вызываем ее, как обычную глобальную функцию — разве что уточняем имя класса, которому она принадлежит. Существование индексного файла проверяется в следующем фрагменте:
// Существует ли индексный файл? CFileStatus cfsDummy; if (CFile::GetStatus(csIndex, cfsDummy) == 0) { // Нет, поэтому создать его (а также файл сообщений) TRACE(_T("Index file not found - being created\n")); if (CreateFiles() == FALSE) { return; } } Если индексный файл не существует, GetStatus возвращает 0. В этом случае мы вызываем функцию CreateFiles, в которой создается индексный файл вместе с файлом сообщений. GetStatus получает и второй параметр — ссылку на объект CFileStatus, в который заносится вся информация о файле. Нас интересует только факт наличия или отсутствия файла, поэтому содержимое этого объекта мы игнорируем. При неудачном завершении функция CreateFiles возвращает FALSE, в этом случае работа ReadIndexFile просто завершается — пока трудно сделать что-либо более осмысленное. Открытие файла все еще может закончиться неудачно, однако это уже будет следствием возникшей ошибки, а не отсутствия файла, поэтому я заменил трассировочное сообщение в условии else на
TRACE(_T("Cannot open the index file")); Функция CreateFiles выглядит так:
BOOL CFirstCtrl::CreateFiles(void) { CFile cfFile; CString csFile; BOOL bRet = FALSE; // Сначала индексный файл csFile.LoadString(IDS_INDEXFILE); if (cfFile.Open(csFile, CFile::modeCreate |
www.books-shop.com
{
CFile::modeWrite | CFile::shareExclusive) == 0)
TRACE(_T("Error creating index file\n")); } else { cfFile.Close(); // Мгновенное закрытие: необходимо // только создать файл. } // Затем файл сообщений csFile.LoadString(IDS_MESSAGEFILE); if (cfFile.Open(csFile, CFile::modeCreate | CFile::modeWrite | CFile::shareExclusive) == 0) { TRACE(_T("Error creating message file\n")); } else { cfFile.Close(); bRet = TRUE; FireFilesCreated(); } return bRet; } В этой функции нет ничего сверхъестественного. Она использует флаг CFile::modeCreate, чтобы сообщить функции Open о необходимости создания файла, и делает это сначала для индексного файла, а затем для файла сообщений. В обоих случаях созданные файлы сразу же закрываются, поскольку на этой стадии нам было нужно лишь создать их. Разумеется, созданный и немедленно закрытый файл имеет нулевую длину. Это означает, что время от времени функции ReadIndexFile придется иметь дело с пустыми индексными файлами. Если вернуться к исходному тексту, можно увидеть, что по длине файла программа определяет количество записей в нем, чтобы выделить под массив область памяти правильного размера. Возникает проблема: если количество записей равно 0 (для только что созданного файла), этот фрагмент будет пытаться выделить 0 байт. 32-разрядный Visual C++ позволяет это сделать; он лишь выводит в отладчике предупреждающее сообщение. Однако 16-разрядный Visual C++ ведет себя менее дружелюбно — в нем срабатывает ASSERT, отчего элемент (в отладочном построении) вообще перестает работать. Это весьма прискорбно, поскольку правила языка C++ не запрещают выделять область размером 0 байт. Предотвратить эту опасность несложно, к тому же это вполне соответствует хорошей практике «защищенного программирования». Нам придется изменить строку, в которой мы вычисляем количество записей в индексном файле и присваиваем его переменной. Ранее строка выглядела так:
m_lHRESULTs = cfIndex.GetLength() / ENTRYSIZE; Теперь она заменяется следующим фрагментом: if ((m_lHRESULTs = cfIndex.GetLength() / ENTRYSIZE) == 0) { TRACE (_T("The index file is empty\n")); return; } Если файл оказывается пустым, мы выводим отладочное сообщение, но не предпринимаем никаких дальнейших действий. Давайте проведем эксперимент. Постройте элемент с этими изменениями, затем переименуйте или удалите индексный файл с файлом сообщений. Запустите тестовый контейнер и включите режим отображения событий командой View|Event Log. Вставьте в тестовый контейнер элемент First. Как ни странно, на первый взгляд ничего не происходит, а в элементе отображается сообщение о недопустимом значении HRESULT. При помощи Windows Explorer найдите индексный
www.books-shop.com
файл с файлом сообщений — все правильно, файлы присутствуют. Они пусты (имеют нулевую длину), поэтому текущее значение HResult оказывается недопустимым. Более того, недопустимым окажется любое введенное значение HRESULT. Вы можете загрузить определения HRESULT из файла WINERROR.H — выполните в тестовом контейнере команду Edit|Invoke Methods и выберите метод BatchLoad, после чего измените значение свойства HResult, чтобы заново просканировать файл и вывести правильное сообщение (к концу главы мы исправим этот недостаток). Но почему же не было возбуждено событие? При других обстоятельствах я бы решил, что представилась идеальная возможность продемонстрировать методику отладки элемента, но сейчас я хочу ограничиться только событиями (об отладке речь пойдет в главе 10). Все дело в том, что элемент вызывает функцию FireFilesCreation из конструктора объекта, производного от COleControl. На этой стадии контейнер и элемент почти не взаимодействуют друг с другом. В частности, контейнер не успел подключиться к точке соединения элемента, так что возбуждаемое событие просто некому передать. Поэтому мне пришлось изобретать механизм, описанный ниже, который позволяет как можно быстрее организовать доставку событий. Каждый раз, когда адресат события подключается к точке соединения, он вызывает IConnectionPoint::Advise. MFC сообщает об этом элементу (экземпляру класса COleControl) через функцию OnEventAdvise. Эта функция вызывается при каждом подключении и при каждом разрыве подключения; по ее параметру-флагу можно судить о том, что же именно происходит при вызове. Версия этой функции из базового класса не делает ничего, однако она объявлена виртуальной, так что мы можем переопределить ее и сделать то, что считаем нужным. Другая виртуальная функция класса COleControl, OnFreezeEvents, вызывается при обращении к методу IOleControl::FreezeEvents элемента. Ей также передается параметр логического типа, который сообщает о том, блокируются или разблокируются события. Теоретически метод FreezeEvents может быть вызван несколько раз подряд, так что реализующий его код должен увеличивать значение счетчика, если параметр равен TRUE, и уменьшать его, если параметр равен FALSE. Переопределив версию OnFreezeEvents базового класса, мы получим счетчик, по значению которого можно сразу определить факт блокировки событий. Из всего сказанного следует, что, переопределив функции OnFreezeEvents и OnEventAdvise, я смогу гарантировать, что событие FilesCreated будет послано всем приемникам, подключенным к точке соединения для событий элемента First. Перейдем к рассмотрению кода. Сначала мы немного поработаем над заголовочным файлом класса CFirstCtrl. В него необходимо добавить объявления для двух переопределенных виртуальных функций (на самом деле ClassWizard сделает это за вас и к тому же добавит «болванки» переопределенных функций в конец CPP-файла — достаточно перейти на вкладку Message Maps, найти функции в списке Messages и сделать двойной щелчок на каждой из них).
virtual void OnFreezeEvents(BOOL bFreeze); virtual void OnEventAdvise(BOOL bAdvise); Затем добавим в него объявления двух переменных:
BOOl m_bFilesCreated; BOOL m_nEventsFrozen; Первая переменная — флаг, показывающий, что файлы были созданы и поэтому нужно возбудить событие; значение второй переменной равно количеству вызовов IOleControl::FreezeEvents с параметром TRUE за вычетом количества вызовов IOleControl::FreezeEvents с параметром FALSE (счетчик блокировки событий; значение 0 означает, что события не заблокированы). Добавим в конструктор CFirstCtrl фрагмент, в котором m_bFilesCreated будет присваиваться значение FALSE, а m_nEventsFrozen — 0. Теперь заменим функцию инициирования события в CreateFiles установкой флага:
m_bFilesCreated = TRUE;
// Приводит к возбуждению события
Наконец, реализуем переопределенные виртуальные функции:
void CFirstCtrl::OnEventAdvise(BOOL bAdvise) { if (bAdvise && m_bFilesCreated && (m_nEventsFrozen == 0)) { FireFilesCreated(); } } OnFreezeEvents увеличивает или уменьшает счетчик в зависимости от параметра bFreeze. Вы можете изменить OnFreezeEvents и вставить дополнительную проверку, которая бы гарантировала, что счетчик никогда не принимает отрицательных значений — такое может произойти, если ошибка в контейнере заставит его вызвать метод FreezeEvents с параметром FALSE до того, как он будет вызван с параметром TRUE. Функция OnEventAdvise проста и прямолинейна. Если параметр показывает, что вызов метода произошел при подключении (а не при разрыве), флаг m_bCreateFiles установлен, а события не заблокированы, возбуждается событие. Оно будет получено по всем новым подключениям, а изза мультиплексирования при каждом новом подключении событие будет направляться всем приемникам, подключенным к точке соединения. Это означает, что «первый» приемник получит событие столько раз, сколько всего приемников будет подключено к элементу, а «последний» — всего один раз. Я поставил слова «первый» и «последний» в кавычки, поскольку реализация точки соединения сама решит, должна ли итерация по списку приемников для данной точки возвращать список в том порядке, в котором происходили подключения. После этого мне захотелось изменить процедуру вызова другого события так, чтобы она тоже учитывала возможность блокировки событий контейнером. Для этого я заменил строку вызова функции события в SetHResult следующим фрагментом:
if (m_nEventsFrozen == 0) { FireInvalidHResult(nNewValue); } Теперь перед вызовом функции FireInvalidHResult мы проверяем, не заблокированы ли события. Элемент все еще обладает двумя заметными недостатками. Во-первых, сообщения об ошибках сделаны просто безобразно, даже после того, как мы слегка улучшили ситуацию и заставили элемент инициировать события при попытке присвоить свойству HResult недопустимое значение. Во-вторых, допустимость HResult (а следовательно, и значения связанных с ним свойств) не проверяется заново при вызове метода Add или BatchLoad. Первый недостаток станет темой следующей главы, а со вторым можно разобраться сейчас. Дело в том, что вызов методов Add и BatchLoad может привести к появлению в индексном файле текущего значения свойства HResult, ранее отсутствовавшего там — стоит проверить допустимость заново. Тем не менее делать это стоит лишь в том случае, если текущее значение HResult помечено как недопустимое, к тому же нет никакого смысла заново возбуждать событие InvalidHResult, если значение остается недопустимым после вызова метода. Чтобы исправить этот недостаток, мы перенесем обобщенный код из SetHResult в более удобную для повторного использования функцию CheckHResult. Функция возвращает TRUE, если значение HResult стало допустимым, и FALSE в противном случае, кроме того, она присваивает нужное значение переменной m_bIsValid. Также она присваивает правильные значения зависимым свойствам — таким, как строки символического имени и сообщения. Эта функция выглядит так:
if (FindEntry(nNewValue, &ulOffset)) { bRet = TRUE; GetInfo(ulOffset); csLine = m_csSymbol + _T(": ") + m_csMessage; } else { csLine.LoadString(IDS_NOVALID_HRESULT); } SetText(csLine); return bRet; } Не забудьте объявить функцию CheckHResult в заголовочном файле! Единственное отличие кода старой функции SetHResult заключается в нескольких тривиальных перестановках. Сама функция SetHResult заметно упрощается:
void CFirstCtrl::SetHResult(long nNewValue) { if (CheckHResult(nNewValue) == FALSE) { if (m_nEventsFrozen == 0) { FireInvalidHResult(nNewValue); } } m_HResult = nNewValue; SetModifiedFlag(); } Чтобы методы Add и BatchLoad при необходимости могли выполнить проверку допустимости и обновить текст элемента, необходимо слегка изменить их. В Add добавляется следующий фрагмент:
if (m_bIsValid == FALSE) { CheckHResult(m_HResult); } непосредственно перед оператором
return TRUE; в конце функции. Тот же самый фрагмент добавляется и в функцию BatchLoad внутри фрагмента, управляемого оператором
if (lEntries) непосредственно перед оператором
return lEntries; в конце функции. Вставьте элемент в тестовый контейнер и присвойте HResult недопустимое значение. Теперь можно добавить запись методом Add — вы увидите, как происходит обновление элемента. Существует и другая возможность — загрузить целую группу записей из файла. Если среди них найдется значение, совпадающее с текущим значением свойства HResult, элемент обновляется, и в нем появляется новый текст. Новая версия элемента First со всеми исправлениями, сделанными в этой главе, находится на диске CD-ROM, в каталоге \CODE\CHAP08\FIRST.
8.9 Реализация событий без MFC Если вы пишете элемент на С++ и не пользуетесь MFC, большую часть работы приходится выполнять самостоятельно. В частности, вам придется определить интерфейс событий в библиотеке типов элемента и обновлять его по мере добавления новых событий, а также написать код для их возбуждения (на самом деле библиотека ATL и ее мастера выполняют немалую часть этой работы). Все остальное, что было сказано в этой главе, включая блокировку событий, остается справедливым.
www.books-shop.com
Глава
9
Ошибки и исключения С этой главы мы начинаем жить по-новому. До сих пор я программировал весьма небрежно и ссылался на то, что мы еще не рассматривали ошибки и исключения. Вся глава посвящена ошибкам и исключениям, а также их влиянию на работу элементов ActiveX, так что на будущее у меня уже не остается подобных оправданий. Соответственно, программирование несколько усложняется, хотя итоговый код будет легче прочитать, и к тому же он работает более надежно. Именно в этом и заключается основной смысл обработки ошибок — программа становится более надежной и лучше справляется с ситуациями, которые не должны ей встречаться в обычных условиях. Трудно гарантировать, что ваш элемент управится с любыми жизненными невзгодами, однако «защищенное программирование» и средства обработки исключений C++ (и аналогичные средства библиотеки MFC, если вы ею пользуетесь), а также информационные возможности COM, используемые Automation и элементами ActiveX, позволяют застраховаться от самых обычных из необычных ситуаций! Прежде всего я расскажу, что же именно понимается под термином «исключение». Затем мы посмотрим, как перехватить исключение в элементе ActiveX и как сообщить о нем пользователю. Наконец, мы переделаем некоторые части элемента First, над которым работаем на протяжение последних глав, и повысим его устойчивость к ошибкам. Многие приемы, рассмотренные в этой главе, можно использовать в любом приложении на C++, а некоторые из них относятся как к элементам, так и к стандартным серверам Automation.
9.1 Что такое «исключение»? С программной точки зрения исключение лучше всего определить как событие, которое происходит независимо от вашего желания, — например, нарушение работы локальной сети или сбой данных на диске. Ввод пользователем неверной информации (например, ввод числа, лежащего за пределами допустимого диапазона) обычно не считается исключением. Конечно, он тоже происходит независимо от вас, однако вы можете предусмотреть эту ситуацию в своей программе и должным образом отреагировать на нее. В случае элемента First исключения могут возникать при нехватке памяти, ошибках работы с файлами и передаче неверных параметров. Мы изменим элемент First так, чтобы он пользовался средствами обработки исключений библиотеки MFC и C++ и по возможности деликатно сообщал о них приложению-контейнеру. Предполагается, что мы живем в 32-разрядном мире и потому можем пользоваться стандартными конструкциями C++ для обработки исключений — try и catch. В первом издании книги все было иначе: каждый элемент должен был работать и в 16-разрядной Windows, поэтому мне приходилось пользоваться макросами TRY и CATCH библиотеки MFC. Ими можно пользоваться и в 32-разрядной среде, где они работают по аналогии с ключевыми словами. Отличие заключается в том, что макросы позволяют обойтись без удаления объектов исключений, а в стандартных исключениях C++ такое удаление является обязательным. Эти макросы удачно имитируют исключения C++ без поддержки компилятора (например, 16-разрядный компилятор Microsoft не поддерживает обработки исключений C++).
9.2 Обработка исключений в MFC и C++ Многие функции MFC способны инициировать исключения; хорошим примером служит оператор new. В справочной системе говорится, что этот оператор может инициировать исключение CMemoryException. Это означает, что при неудачной попытке выделения памяти оператор new не возвращает 0, а создает объект CMemoryException, инициализирует его так, чтобы описать возникшую проблему, прерывает стандартный порядок выполнения программы и передает управление ближайшему catch-блоку, в котором перехватывается CMemoryException или исключение его базового класса CException. При включенной обработке исключений C++ при таком переходе также обычно уничтожаются все объекты, созданные между точками
www.books-shop.com
инициирования и перехвата. Я говорю «обычно», потому что большинство компиляторов C++ позволяет отключить обработку исключений для отдельных функций — все хорошо, если только в этих функциях не создаются объекты. Даже если сама функция не инициирует и не перехватывает исключения, она должна произвести необходимую «раскрутку стека» (и следовательно, иметь разрешенную обработку исключений), если вызванная ей функция инициирует исключение, которое выходит за пределы вызывающей функции. Если в вашем компиляторе не поддерживаются средства обработки исключений C++ (например, 16-разрядный Microsoft Visual C++ версии 1.5x), макросы постараются удалить динамически созданные объекты, однако в некоторых ситуациях они оказываются бессильны. Как перехватываются исключения? Давайте представим себе, что наша программа выделяет область памяти для объекта оператором new и мы хотим обрабатывать возможные случаи неудачного вызова new. Код будет выглядеть примерно так:
try {
CMyObject *x = new CMyObject; } catch (CException *e) { AfxMessageBox("Help!"); e -> Delete(); return; } Этот фрагмент сообщает компилятору о том, что все исключения типа CException (или производных от него классов), инициированные операторами внутри try- блока, должны обрабатываться операторами внутри catch-блока. Итак, если в нашем примере вызов new окажется неудачным, он инициирует CMemoryException. Данный класс исключения является производным от CException, поэтому он перехватывается catch-блоком, который в нашем примере выводит окно сообщения. Обратите внимание на то, что объект исключения удаляется. Если вы забудете сделать это, то при каждом перехвате исключения в этой точке программы будет возникать «утечка памяти». Если исключение не перехватывается ближайшим catch-блоком, оно двигается по цепочке вызовов до тех пор, пока не будет перехвачено. Если обработчик исключения не будет найден в программе, MFC получает его (или runtime-модуль C/C++, если вы не пользуетесь MFC) и завершает работу программы. Это означает, что исключения могут передать управление на несколько функций назад в стеке вызовов или даже во внешнюю функцию программы. Поскольку catch-блоки различаются по типу перехватываемых исключений, вы можете предусмотреть свой вариант обработки для каждой разновидности исключений. Например, функция, которая выделяет область памяти, открывает файл и читает из него данные, может закрывать файл при инициировании файлового исключения, но не делать этого для исключений, связанных с выделением памяти. Вы даже можете обрабатывать некоторые исключения в той функции, где они произошли, а другие — в функциях более высокого уровня. При желании можно действовать совсем хитро — например, выполнить локальный перехват исключения, произвести некоторые локальные действия по «уборке мусора» и затем заново инициировать его оператором throw, чтобы исключение попало в функцию более высокого уровня, или даже инициировать вместо него другое исключение. Обработка исключений C++ позволяет инициировать и перехватывать любые типы объектов. Макросы обработки исключений из библиотеки MFC работают только с объектами класса CException и производных от него. В MFC определено немало классов исключений, среди которых — исключения для выделения памяти, для файловых операций, действий с базами данных, COM и OLE, Automation и операций с ресурсами Windows. Все эти классы являются производными от CException и могут перехватываться catch-блоками для конкретных исключений или catch-блоками для обобщенного исключения CException, как в приведенном выше случае (или даже в варианте catch(...), при котором перехватываются все исключения независимо от типа). Поскольку функция может инициировать исключения различных типов, операторы catch (или макросы MFC CATCH) могут объединяться в цепочки. Следовательно, если функция MyFunc может
www.books-shop.com
инициировать исключения классов CMemoryException, CFileException и COleException, они могут обрабатываться одним общим catch-блоком для CException или же по отдельности:
try {
MyFunc(); } catch (CMemoryException *e) { // Фрагмент для обработки исключений, // связанных с выделением памяти e -> Delete(); // Если исключение не инициируется заново } catch (CFileException *e) { // Фрагмент для обработки файловых исключений e -> Delete(); // Если исключение не инициируется заново } catch (COleException *e) { // Обработка исключений OLE e -> Delete(); // Если исключение не инициируется заново } Кстати говоря, параметр оператора catch представляет собой имя объекта исключения в том виде, в котором оно будет употребляться внутри блока. Обычно используется указатель на объект, а его имя выбирается произвольно. Я привык называть его e (чтобы нажимать поменьше клавиш). В макросах MFC для обработки прерываний синтаксис макроса CATCH отличается от синтаксиса оператора сatch, поскольку макрос получает два параметра: тип объекта исключения (например, CMemoryException) и имя указателя на него. Вы можете создавать собственные объекты исключений, производные от CException. Это может пригодиться, если класс исключений встречается только в вашем приложении или же вы хотите преобразовать исключение одного типа в другой, чтобы одинаково обрабатывать исключения разных типов. В частности, исключения производного класса встречаются в новой версии элемента First, приведенной в этой главе. И последнее, о чем стоит сказать: конечно, вам придется написать код для обработки исключений, а значит, придется больше трудиться. К тому же компилятор также вставляет дополнительный код, так что ваши программы увеличиваются в размерах. Преимущества заключаются в том, что программа становится более надежной. Она переживет возникшие трудности с большей вероятностью, чем программа без обработки исключений. Приготовьтесь и к некоторому снижению производительности, поскольку многие вставки компилятора выполняются и без инициируемых исключений. Это считается неизбежным злом, поэтому каждый нормальный компилятор, поддерживающий обработку исключений, позволяет запретить ее! С обработкой исключений связана целая философия. Некоторые программисты рассматривают ее как закон, которому следует беспрекословно подчиняться, а другие считают гнусным извращением. Думаю, истина лежит где-то посередине. В первом издании книги я переписал элемент First так, чтобы он перехватывал чуть ли не каждое теоретически возможное исключение. Например, я перехватывал исключения, связанные с выделением памяти, при некоторых операциях с классом CString, где выделяемый объем настолько мал, что вероятность неудачи просто ничтожна. Не стоит забывать и о другом — если вам не хватает памяти на выделение нескольких байтов для строки, то наверняка ее не хватит и для самого объекта исключения! Если для MFC исключения имеют чрезвычайно большое значение, то другие средства C++ (например, библиотека ATL, предназначенная для создания компактных и быстрых элементов) обычно стараются не иметь дела с исключениями. Философия ATL — включать лишь то, что действительно необходимо. Переделывая эту главу, я постарался более рационально подойти к обработке исключений. И все же решение вам придется принимать самостоятельно — если вы готовы смириться с падением
www.books-shop.com
производительности и увеличением размеров, то обработка исключений сделает ваши объекты более надежными. Если вас это не устраивает, придется самостоятельно обрабатывать аномальные ситуации (как в старом добром языке C) или же писать заведомо ненадежный код. Пожалуй, последний вариант выглядит несколько нереалистично.
9.3 Обработка исключений в элементах ActiveX Не беспокойтесь — я вовсе не забыл, что наша книга посвящена элементам ActiveX. Впрочем, все сказанное выше окажется полезным и при их разработке. Прибавьте то, о чем я расскажу сейчас, — и вы получите мощную, надежную модель, ориентированную как на разработчика, так и на пользователя элементов ActiveX. В главах 2 и 3 говорилось о том, что серверы Automation могут передавать своим контроллерам исключения, содержащие разнообразную информацию — код ошибки, справочный файл с дополнительными сведениями и текст ошибки. Конечно, работа элементов ActiveX в значительной степени основана на Automation, так что нет ничего удивительного в том, что элементы также могут посылать своим контейнерам исключения Automation. Тем не менее между классическим сервером Automation и элементом ActiveX существует одно важное отличие. Сервер Automation общается с контроллером только во время вызова метода или обращения к свойству, то есть по желанию контроллера. Элемент ActiveX может связаться со своим контейнером в любой момент, инициировав событие. Поскольку событие никак не зависит от обращения со стороны контейнера, исключения инициируются по несколько иным правилам. В общем случае справедливы следующие утверждения:
Если исключение возникает при вызове метода или обращении к свойству, следует инициировать исключение Automation (ThrowError в программе на MFC). В любой другой момент исключение инициируется при помощи стандартного события Error, о котором рассказывается ниже (FireError в программе на MFC).
Функции ThrowError и FireError принадлежат классу COleControl и получают одинаковые параметры: значение HRESULT для исключения, строку (или идентификатор строкового ресурса) и идентификатор справочного контекста. Вызов ThrowError приводит к созданию особой разновидности стандартного объекта COleDispatchException, используемого в MFC для исключений Automation. Эта разновидность, COleDispatchExceptionEx, сообщает исходному вызову IDispatch::Invoke о возникновении исключения так, что он может передать сообщение пользовательскому коду или обработать его самостоятельно. Например, тестовый контейнер из Microsoft Visual C++ 4.2 при получении исключения от внедренного элемента просто выдает звуковой сигнал. Конечно, большинство контейнеров все же более вразумительно сообщает о возникших проблемах! Функция FireError представляет больший интерес, она инициирует стандартное событие Error. Событие Error имеет dispid –608 (DISPID_ERROREVENT, определяется в OLECTL.H) и несколько параметров:
Номер ошибки, короткое целое. Описание ошибки, указатель на BSTR. HRESULT ошибки. Источник в виде BSTR. Справочный файл в виде BSTR. Идентификатор справочного контекста, длинное целое. Указатель на логическую переменную; если получатель события присваивает ей TRUE, то элемент не выводит сообщение об ошибке.
Последний параметр позволяет элементу вывести сообщение об ошибке, если контейнер не желает этого делать. Полагаю, большинство контейнеров все же предпочитает брать на себя вывод и/или обработку ошибок. Тем не менее некоторые контейнеры могут поручать эту задачу элементу. Оставляя последний параметр равным FALSE, контейнер указывает элементу на то, что тот должен вывести сообщение об ошибке. В элементах на базе MFC это приводит к вызову функции DisplayError, которая по умолчанию отображает окно сообщения. Данная функция является виртуальной, поэтому ее поведение может быть переопределено в производном классе элемента ActiveX.
www.books-shop.com
Документация по MFC предостерегает против намеренного вызова функции FireError и рекомендует использовать в элементах другие средства (например, HRESULT) для того, чтобы сообщить контейнеру об ошибке. Поскольку это не может быть сделано асинхронно (то есть без предварительного обращения к элементу со стороны контейнера), неизбежно будут возникать ситуации, при которых событие Error останется единственной возможностью для общения с контейнером. В таких случаях элементам на базе MFC следует вызывать FireError. В элементе First имеется лишь несколько ситуаций, при которых должно возбуждаться событие Error. Большая часть исключений, перехватываемых First, возникает при вызовах методов или обращениям к свойствам, а при этих обстоятельствах можно вызвать ThrowError.
9.4 Исключения и двойственные интерфейсы Все, что говорилось выше про обработку ошибок и исключений в Automation, остается справедливым до тех пор, пока вы работаете через IDispatch::Invoke. С появлением интерфейса Automation, который работает как через v-таблицу, так и через IDispatch, ситуация несколько усложняется. Прежде всего, в этом случае не существует прямого механизма для возврата исключений вызывающей стороне — этот сервис предоставляет Invoke. Отказываясь от Invoke, вы отказываетесь и от его сервиса. Впрочем, это не означает, что для двойственных интерфейсов не существует аналогичных средств. Просто вам (и тому фрагменту программы, который обращается к вашему интерфейсу) придется чуть больше потрудиться. Возможно, вы еще помните из нашего обсуждения двойственных интерфейсов и примера в главе 3, что методы и функции доступа к свойствам для двойственных интерфейсов похожи на обычные методы COM-интерфейсов — в частности, они также возвращают HRESULT. Следовательно, в простейшем случае объект Automation может решить, что он не будет поддерживать разнообразную информацию об исключениях, которую могут предоставлять объекты Automation, если объект не вызывается через IDispatch. Тогда для методов, вызываемых через v-таблицу, объект просто возвращает соответствующий HRESULT. Однако подобное решение вряд ли можно назвать удачным, так как пользователи контроллеров Automation желают всегда получать полную информацию об ошибках и исключениях, независимо от способа обращения к объекту. Чтобы улучшить ситуацию, необходимо кое-что сделать. Прежде всего, ваш объект должен поддерживать интерфейс с именем ISupportErrorInfo. Данный интерфейс является производным от IUnknown и содержит один дополнительный метод InterfaceSupportsErrorInfo. В качестве параметра ему передается IID интересующего вас интерфейса объекта. Если он поддерживает полную информацию об ошибках в стиле Invoke, метод возвращает S_OK, в противном случае он должен возвращать S_FALSE. Пока все просто. Теперь, если вы собираетесь положительно отвечать на подобные запросы, необходимо подготовить объект с информацией об ошибке, откуда контроллер мог бы ее получить. Это можно сделать несколькими способами (подробности приведены в документации по Automation), но самый простой из них выглядит так:
В момент отказа вызвать CreateErrorInfo (функция Automation API) для создания объекта ошибки. Функция возвращает указатель на системную реализацию интерфейса ICreateErrorInfo. Заполнить объект ошибки, пользуясь методами интерфейса ICreateErrorInfo — такими, как SetDescription. Вызвать QueryInterface для объекта ошибки и запросить у него интерфейс IErrorInfo. Передать указатель на IErrorInfo функции SetErrorInfo (еще одна функция Automation API), которая позволяет контроллеру получить объект ошибки функцией GetErrorInfo.
При таком подходе контроллер (в данном случае — контейнер вашего элемента) может получить одну и ту же информацию об исключении независимо от того, как используется ваш элемент. Если вы реализуете двойственный интерфейс для обращения к свойствам и методам вашего элемента, я бы настойчиво посоветовал заодно реализовать и правильную обработку исключений Automation.
9.5 Обработка исключений элементом First
www.books-shop.com
Последняя версия нашего элемента вполне нормально работала, однако у нее были определенные проблемы с надежностью и устойчивостью работы. Более того, многие ошибки и исключения, о которых сообщалось контейнеру, практически не давали полезной информации о том, что же именно случилось. В новой версии элемента First предусмотрена обработка многих исключений — как инициирование, так и перехват с последующей обработкой информации. Наверное, даже после внесенных изменений еще остаются возможности для усовершенствований, но я их не нашел. Как упоминалось выше, я отказался от лишней обработки исключений при выделении мелких областей памяти. Исходный текст новой версии элемента находится на прилагаемом CD-ROM, в каталоге \CODE\CHAP09\FIRST. Изменения приведены в листингах с 9-1 по 9-4 (исправленные варианты файлов FIRSTCTL.H и FIRSTCTL.CPP и новые FIRSTEX.H и FIRSTEX.CPP). Кроме того, в листингах отсутствуют некоторые изменения, внесенные в ресурсы строковой таблицы. Листинг 9-1. Файл FIRSTCTL.H с обработкой исключений
/////////////////////////////////////////////////////////////// // Информация типа для элемента static const DWORD BASED_CODE _dwFirstOleMisc = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE | OLEMISC_RECOMPOSEONRESIZE; IMPLEMENT_OLECTLTYPE(CFirstCtrl, IDS_FIRST, _dwFirstOleMisc) /////////////////////////////////////////////////////////////// // CFirstCtrl::CFirstCtrlFactory::UpdateRegistry // Добавляет или удаляет записи реестра для CFirstCtrl BOOL CFirstCtrl::CFirstCtrlFactory::UpdateRegistry(BOOL bRegister) { // Убедитесь, что ваш элемент соответствует требованиям // совместной потоковой модели. Подробности приведены // в документе MFC TechNote 64. // Если элемент нарушает требования совместной модели, // необходимо модифицировать следующий фрагмент программы // и заменить 6-й параметр с afxRegApartmentThreading на 0.
Если при чтении индексного файла возникает исключение, у нас большие проблемы. Предупредить пользователя о том, что с элементом стряслось что-то серьезное, и продолжить (из конструктора нельзя инициировать исключение или вернуть код ошибки).
try {
}
ReadIndexFile(); } catch (CException *e) { CString csExtra; UINT uStr = 0; if (e -> IsKindOf(RUNTIME_CLASS(CFirstException))) { ((CFirstException *)e) -> GetErrorString(uStr); } else if (e -> IsKindOf(RUNTIME_CLASS(CFileException))) { GetFileExceptionString((CFileException *)e, uStr); } if (uStr) { csExtra.LoadString(uStr); m_csBadMessage += _T("\n\nActual error message:\n\n"); m_csBadMessage += csExtra; } AfxMessageBox(m_csBadMessage, MB_OK); if (uStr) { m_csBadMessage.LoadString(IDS_BADMESSAGE); } e -> Delete(); }
catch (CMemoryException *e) { e -> Delete(); DoError(CTL_E_OUTOFMEMORY, IDS_MEMORYERROR, 0); } m_bInDispatch = FALSE; return lEntries; } // Не перехватывает исключений, поэтому может инициировать // все, что инициируется в вызываемых ею функциях. // Сюда входят исключения CFirstException, // CFileException и CMemoryException. long CFirstCtrl::DoBatchLoad(CStdioFile *cfIn, CFile *cfIndex, CFile *cfMsg) { long lEntries = 0; cfIndex -> Seek(0, CFile::end); CString csLine, csMsg, csSymbol; while (GetNextDefineLine(cfIn, &csLine, &csMsg)) { long lCode = GetTheCode(&csLine, &csSymbol); unsigned long ulOffset; if (FindEntry(lCode, &ulOffset)) { TRACE1(_T("HRESULT %08X already in database - ignored\n"), lCode); } else { long lMsgPos = cfMsg -> Seek(0, CFile::end); cfIndex -> Write(&lCode, sizeof(lCode)); cfIndex -> Write(&lMsgPos, sizeof(lMsgPos)); cfMsg -> Write((LPCTSTR)csSymbol, csSymbol.GetLength() + 1); cfMsg -> Write((LPCTSTR)csMsg, csMsg.GetLength() + 1); ++lEntries;
}
} } return lEntries;
// Также передает любые исключения вызвавшей функции (может // передавать как минимум CFirstException, // CFileException и CMemoryException.) BOOL CFirstCtrl::GetNextDefineLine(CStdioFile *cfFile, CString *csLine, CString *csMessage) { _TCHAR szBuf[256]; CString csCompare; BOOL bFound = FALSE; LPTSTR lpszCnt; do { lpszCnt = cfFile -> ReadString(szBuf, 255); if (lpszCnt == NULL) { break; } csCompare = szBuf;
www.books-shop.com
bFound = (csCompare.Find(_T("// MessageText:")) != -1); } while (bFound == FALSE); if (bFound) { // Пропустить пустую строку комментария cfFile -> ReadString(szBuf, 255); // Получить строку (строки) сообщения csMessage -> Empty(); do { cfFile -> ReadString(szBuf, 255); if (szBuf[3]) { if (!csMessage -> IsEmpty()) { *csMessage += _T(" "); } szBuf[_tcslen(szBuf) - 1] = TCHAR(0); *csMessage += szBuf + 4; } } while (szBuf[3]); // Получить строку кода lpszCnt = cfFile -> ReadString(szBuf, 255); if (lpszCnt == NULL) { TRACE(_T( "The file given to BatchLoad is in the wrong format\n")); throw new CFirstException (CFirstException::badCodesFile); } *csLine = szBuf; return TRUE; }
} return FALSE; long CFirstCtrl::GetTheCode(CString *csLine, CString *csSymbol) { // Если ‘#define’ отсутствует или находится // не в начале строки, файл не был создан утилитой MC if (csLine -> Find(_T("#define"))) { TRACE(_T( "#define line doesn’t start with exactly ‘#define’\n")); throw new CFirstException(CFirstException::badCodesFile); } // Пропустить ‘#define’ int i = 7; // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define’\n")); throw new CFirstException(CFirstException::badCodesFile);
www.books-shop.com
} // Получить символическое имя csSymbol -> Empty(); while ((csLine -> GetLength() > i) && !(_istspace(csLine -> GetAt(i)))) { *csSymbol += csLine -> GetAt(i); ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define SYMBOL’\n")); throw new CFirstException(CFirstException::badCodesFile); } // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define SYMBOL’\n")); throw new CFirstException(CFirstException::badCodesFile); } // В последних версиях файла WINERROR.H номер ошибки // может быть представлен в виде _HRESULT_TYPEDEF(номер), // в этом случае следует пропустить имя макроса. int pos; if ((pos = csLine -> Find(_T("_HRESULT_TYPEDEF_("))) != -1) { i = pos + 18; // Длина имени макроса равна 18 }
}
// Получить номер CString csNumber; csNumber = csLine -> Mid(i); // Мы не сможем легко сообщить об ошибках, // которые происходят в этом месте (см. текст) return _tcstoul(csNumber, NULL, 0);
case CFileException::tooManyOpenFiles: hr = CTL_E_TOOMANYFILES; break; case CFileException::invalidFile: hr = CTL_E_BADFILENAMEORNUMBER; break; case CFileException::directoryFull: case CFileException::diskFull: hr = CTL_E_DISKFULL; break; case CFileException::badSeek: case CFileException::hardIO: hr = CTL_E_DEVICEIOERROR; break; case CFileException::accessDenied: case CFileException::removeCurrentDir: case CFileException::sharingViolation: case CFileException::lockViolation: hr = CTL_E_PERMISSIONDENIED; break; case CFileException::endOfFile: hr = CTL_E_BADRECORDLENGTH; break; default: hr = CTL_E_ILLEGALFUNCTIONCALL; uStr = IDS_UNKNOWNEXCEPTIONCAUSE; break; }
Переменная m_bInDispatch определяет, когда произошло исключение, о котором сообщается контейнеру, — во время вызова метода или обращения к свойству из контейнера (в этом случае применяется ThrowError) или же асинхронно, за пределами стандартных функций Automation (в этом случае применяется FireError). Переменная m_csBadMessage содержит сообщение, которое элемент выводит пользователю в начале своей работы, если ему не удается открыть или создать индексный файл и файл сообщений. Функция DoError сообщает об исключении контейнеру посредством функции ThrowError или FireError, в зависимости от значения переменной m_bInDispatch. Функция ReallySetHResult содержит код старой функции SetHResult, поскольку ReallySetHResult может вызываться как контейнером (из функции SetHResult при обращении к свойству), так и элементом (при обеспечении устойчивости), следовательно, исключения, о которых она должна сообщать, также могут происходить как внутри вызовов методов и обращений к свойствам Automation, так и за их пределами. GetFileExceptionString — простая функция просмотра, которая преобразует код причины из объекта CFileException в содержательную строку (я взял коды исключений из заголовочного файла для CFileException и самостоятельно добавил строки в строковую таблицу). Перейдем к файлу реализации FirstCtrl. Обратите внимание на новую директиву #include для FIRSTEX.H — заголовочного файла, в котором объявляется класс CFirstException. Данный класс является производным от CException и позволяет организовать специализированную обработку исключений в коде элемента. Я опишу класс исключения после того, как закончу обзор изменений в FirstCtrl. Следующее изменение находится в конструкторе CFirstCtrl. Здесь я присваиваю двум новым переменным значения по умолчанию и вызываю функцию ReadIndexFile. Однако эта функция может инициировать исключение, поэтому она заключается в try-блок с перехватом обобщенного CException. Если функция ReadIndexFile завершается неудачно, у элемента возникают большие проблемы — он не может ни найти, ни создать индексный файл и/или файл сообщений. В таком состоянии от него будет мало проку. Вместо того чтобы приказывать элементу уничтожить себя (что было бы невежливо по отношению к пользователю), я решил вывести предупреждающее сообщение и продолжить работу. Для этого на экран выводится окно сообщения с m_csBadMessage независимо от типа исключения — вот почему обработчик перехватывает обобщенное исключение CException. По умолчанию в m_csBadMessage загружается строка, которая в переводе гласит: «Элемент не смог прочитать по крайней мере один из файлов, необходимых для преобразования HRESULT в текст. Хотя вы можете продолжить работу, возможности элемента заметно ограничиваются». Обратите внимание на то, как мы при помощи IsKindOf и RUNTIME_CLASS определяем, относится ли инициированное исключение к классу CFirstException или CFileException (если бы мы не работали с MFC, можно было бы вместо этого воспользоваться RTTI, runtime-информацией типа). Если исключение относится к одному из этих классов, мы выделяем его причину и преобразуем ее в текстовую строку (функцией GetErrorString для класса CFirstException или только что созданной функцией GetFileExceptionString класса CFileException). Текстовая строка заносится в окно сообщения. В данном случае мы не инициируем события Error; все равно из контейнера элемента его никто не сможет перехватить. Выше описан лишь один из возможных способов обработки разнородных исключений. Выбранная мной стратегия заключается в единой обработке любых исключений с дополнительной обработкой для пары конкретных типов. С тем же успехом можно было воспользоваться тремя catch-блоками (по одному для каждого типа исключения), содержимое которых было бы почти одинаковым. Этот вариант также встречается в нашем элементе, поскольку я хотел продемонстрировать все способы обработки ошибок. В функции DoPropExchange произошло небольшое изменение: когда-то функция SetHResult вызывалась в ней напрямую, но теперь ситуацию необходимо изменить — DoPropExchange вызывается без участия метода Automation, и при возникновении исключения нужно возбудить событие, а не исключение Automation. Мы присваиваем флагу m_bInDispatch значение FALSE и вызываем новую функцию ReallySetHResult. Функция SetHResult сокращается до трех строк: мы присваиваем флагу m_bInDispatch значение TRUE (поскольку данная функция может быть вызвана только при работе со свойствами средствами COM), вызываем ReallySetHResult и сбрасываем m_bInDispatch. Теперь весь код, ранее содержавшийся в функции SetHResult, перенесен в функцию ReallySetHResult. Последняя не перехватывает исключений, поскольку CheckHResult не инициирует их и перехватывает те исключения, которые могут возникнуть в вызываемой ею функции (в последнем случае исключение никогда не будет передано в ReallySetHResult).
www.books-shop.com
Все функции чтения свойств остались неизменными, поскольку исключения в них не должны возникать (разве что при выделении памяти, если функция возвращает строку, но я считаю подобные отказы крайне маловероятными). С другой стороны, в методах произошли обширные изменения. Начнем с метода Add: после предварительной проверки присутствия HRESULT в базе данных (при помощи функции FindEntry, не инициирующей исключений) метод присваивает флагу m_bInDispatch значение TRUE и входит в try-блок. Первый оператор внутри блока проверяет правильность строковых параметров, переданных Add. Если параметры неверны, исключение CFirstException инициируется при помощи ключевого слова throw. Обратите внимание на то, что конструктор CFirstException имеет параметр, определяющий «причину» исключения — он принадлежит объекту исключения, так что код catch-блока может определить по нему, что же именно случилось. В данном случае я присваиваю этому параметру константу badParameters из перечисляемого типа, определенного в классе исключения. Условие catch в методе Add (как и в конструкторе) перехватывает все исключения. Я опять выбрал такой способ обработки ошибок, потому что код для различных типов исключений во многом совпадает. И снова я получаю от объекта исключения строку с описанием ошибки, но на этот раз вызываю функцию DoError, которая вызывает ThrowError или FireError (в зависимости от значения m_bInDispatch). В нашем случае вызывается ThrowError. Обратите внимание: я получаю HRESULT от исключения через GetErrorString — ту же функцию, которая возвращала строку с описанием. В обработчике исключения имеется фиктивный код, предназначенный для чисто демонстрационных целей. После вызова ThrowError в ответ на вызов метода Automation управление выходит за пределы элемента. Следовательно, те операторы, которые следуют в данном обработчике за DoError (сброс m_bInDispatch и возврат FALSE), вообще не будут вызываться! Также обратите внимание на то, что в catch-блоках, вызывающих DoError, объект исключения предварительно удаляется перед вызовом этой функции — в противном случае возникает «утечка памяти». Функция BatchLoad также содержит try-блок, в котором находится большая часть ее кода. Она сначала проверяет строковый параметр, а затем пытается открыть файл сообщений, индексный файл и заданный входной файл. Неудача при открытии любого из них приводит к инициированию CFirstException с параметром «причины», определяющим файл, который не удалось открыть. Однако на этот раз функция отдельно перехватывает каждый класс исключений и вызывает DoError с нужной информацией. Решите сами, какая из этих стратегий вам больше нравится. Нередко общий код оказывается более компактным (например, можно обойтись всего одним e -> Delete() для всех исключений). Тем не менее если обработчики исключений разных типов существенно различаются, то усложнение общего кода может свести на нет все преимущества от уменьшения объема (если оно вообще будет). Переходим к ReadIndexFile. В данном случае применяется общий catch-блок, поскольку мы убираем локальный «мусор» перед тем, как передать исключение следующему обработчику в цепочке (то есть обработчику функции, вызвавшей ReadIndexFile, или функции, которая вызвала эту функцию, и так далее) посредством ключевого слова throw без всяких параметров. Оно сообщает компилятору о необходимости инициировать то же самое исключение, вот почему я не стал удалять его. Если функция ReadIndexFile не сможет открыть индексный файл, инициируется исключение CFirstException. Все остальные функции построены по тому же принципу — при возникновении проблем они отправляют исключения «наверх» и предполагают, что они будут перехвачены кодом верхнего уровня. Например, взгляните на DoBatchLoad. С первого взгляда в ней не видно никакой обработки исключений. Тем не менее хотя эта функция сама по себе не инициирует и не перехватывает исключений, она вызывает другие функции, которые могут их инициировать. Соответственно, любое исключение, инициируемое функцией более низкого уровня, проходит через DoBatchLoad перед тем, как попасть в вызвавшую ее функцию BatchLoad. Далее следует функция GetTheCode, которая также была усовершенствована для повышения надежности. Теперь она уже не надеется на то, что файл HRESULT имеет правильный формат, а проверяет его и инициирует исключение при наличии ошибок. Единственное место, где инициирование исключения проходит не так просто, находится в конце этой функции, где GetTheCode вызывает _tcstoul для преобразования строки в число. Данная функция сообщает о неудаче, возвращая 0, который является вполне допустимым числом. Кроме того, эта функция получает параметр, указывающий на первый недопустимый символ, — в этом месте преобразование прекращается. Следовательно, обработка допустимого числа 123L прекращается
www.books-shop.com
на L, а возвращенный результат будет равен 123. С другой стороны, недопустимое число 123X4 также возвратит 123, а указатель будет ссылаться на X. Следовательно, вопрос о том, какое преобразование закончилось удачно, а какое — нет, не так уж тривиален. Я поленился и не стал проверять. Если хотите — сделайте сами! Функция CreateFiles теперь перехватывает все исключения и преобразует их в возвращаемое значение FALSE, так что при возникновении любых проблем эта функция просто сообщает о том, что она не сработала. Функция DoError чрезвычайно проста. Она загружает из строковой таблицы строку с описанием ошибки, решает, что ей следует инициировать — событие или ошибку Automation, после чего именно это и проделывает. События инициируются лишь в том случае, если они не были заблокированы контейнером. GetFileExceptionString преобразует переменную m_cause класса CFileException в соответствующее значение HRESULT и содержательную строку с описанием ошибки. Обратите внимание на то, что моя реализация небезупречна — она предполагает, что все значения m_cause следуют подряд, начиная с нуля. Перечисляемые типы C++, к которым относится m_cause, по умолчанию ведут себя именно так, однако это правило можно переопределить. Просто в текущей реализации CFileException мое предположение оказывается верным! Наконец, мы подошли к классу CFirstException. Для пущей наглядности я реализовал его в отдельном файле FIRSTEX.CPP. Заголовочный файл объявляет этот класс производным от CException (как и все исключения в MFC) и разрешает его динамическое создание (как и для всех исключений в MFC). Затем следует перечисляемый тип, в котором собраны все возможные причины отказов, поддерживаемые данным типом исключений. За ним идет конструктор, он всего лишь инициализирует переменную m_cause. Деструктор тоже почти ничего не делает. В этом классе имеется всего одна настоящая функция GetErrorString, которая преобразует текущее значение m_cause в идентификатор строки и HRESULT. Эта функция определяется в файле FIRSTEX.CPP, а ее работа сводится к обычному просмотру, в ходе которого значение m_cause сравнивается с HRESULT. Я пользуюсь только заранее определенными значениями HRESULT. Вы можете добавить свои HRESULT при помощи макроса CUSTOM_CTL_SCODE, но делать это стоит только при крайней необходимости. И снова идентификатор строки образуется сложением значения m_cause с константой, которая соответствует первому сообщению об ошибке для данного исключения в строковой таблице. Все остальные сообщения последовательно идут за ним.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Вы не сможете скомпилировать эту программу компилятором, не поддерживающим полноценной обработки исключений C++ — например, Microsoft Visual C++ версии 1.5x. В этом случае вам придется воспользоваться старыми макросами MFC, служащими для обработки исключений — TRY, CATCH, THROW и т. д. При уничтожении элемента могут появиться сообщения об утечке памяти. Они встречаются лишь тогда, когда элемент инициировал исключения. Что же происходит? Ответ кроется в классе CString. Объекты CString выделяют память для хранения своих строк из динамически распределяемого пула (heap) оператором new, эта память освобождается при вызове деструктора CString. Без полноценной обработки исключений C++ деструкторы вызываются не всегда, так что память так и остается неосвобожденной. Чтобы избежать утечек, можно явным образом освобождать память функцией CString::Empty при каждом инициировании исключения. Следовательно, если вы пишете 16-разрядные элементы ActiveX, и у вас встречается фрагмент такого вида:
cString csMsg; csMsg.LoadString(IDS_INDEX_FILE); if (DoSomething() == FALSE) { THROW(new CAnException); } вам придется изменить его следующим образом:
THROW(new CAnException); } Я не сделал этого в нашем элементе First, поскольку в этом издании книги предполагается, что все читатели компилируют свои элементы для 32-разрядной модели и работают с компиляторами, поддерживающими обработку исключений C++ и ключевые слова С++ try, catch, throw и т. д. вместо макросов MFC. Оставляю переделку читателям в качестве «домашнего задания»!
Как видите, класс CFirstException устроен достаточно просто, как и большая часть обработки исключений. Реализовать ее не так уж трудно, зато польза оказывается огромной. Вы можете протестировать новую версию элемента First и имитировать различные ошибки, с которыми элемент будет легко и изящно справляться. Я настоятельно рекомендую (в общих случаях) использовать обработку исключений в ваших программах и разумно поступать с перехваченными исключениями. Как упоминалось выше, в отдельных случаях обработка исключений оказывается нежелательной, а иногда приходится даже исповедовать совершенно иную философию, особенно если на первое место выходят размер и скорость элемента. Главное — помнить, что обработка исключений повышает надежность ваших элементов и предоставляет им полную информацию о возникающих ошибках.
9.6 Обработка исключений без использования MFС Если вы не пользуетесь MFC, то не сможете работать с CException и производными от него классами. Тем не менее вы все равно оказываетесь перед выбором: поддерживать обработку исключений C++ или нет? Если нет, руководствуйтесь изложенными выше рекомендациями для элементов на базе MFC, в которых не используется обработка исключений. Если же вы захотите пользоваться исключениями C++, необходимо решить, как именно это сделать. Возможный вариант — имитировать работу всех классов исключений MFC (или по крайней мере тех, которые необходимы в вашей программе). Возможно, вместо этого стоит определить свой собственный набор классов исключений или даже работать с простыми типами. Например, можно инициировать исключение в виде целого числа:
if (что-то плохое) { throw 42; } …… дополнительный код …catch (int e) { if (e == 42) { сделать то, что относится к ошибке 42 } } Фактически мы возвращаемся к числовым кодам ошибок. Это означает, что catch-блоки могут выглядеть несколько громоздко, но они будут простыми и понятными. Вам даже не придется вызывать delete для целого числа, как это необходимо делать для объектов. Разумеется, если вы решитесь использовать эту стратегию, пользуйтесь символическими именами вместо абсолютно бессмысленных чисел вроде «42»!
www.books-shop.com
Глава
10
Консолидация На протяжении нескольких последних глав я постепенно провел вас по пути проектирования и создания работоспособного элемента ActiveX. В каждой главе описывалась какая-то новая концепция — свойства, события или исключения. Теперь вы все знаете и можете приступить к работе над собственными элементами, не так ли? На самом деле так — но если вы действительно хотите научиться, нам предстоит еще многое узнать. Я еще не рассказал о том, как работают элементы в условиях World Wide Web (и не расскажу в этой главе), хотя мощнейшее влияние Web в корне изменило многие основы поведения элементов. Я часто говорил о том, что элемент First обладает недостатками, с которыми нельзя смириться, но при этом так и не показал, как происходит отладка элемента. Поэтому я решил задержаться на половине пути и посвятить целую главу тому, чтобы свести воедино полученные знания, вместо того, чтобы излагать занимательный новый материал. В этой главе рассматриваются некоторые дополнительные аспекты проектирования элементов, отладка, использование ODBC для доступа к данным, справочные файлы, а также проблемы, связанные с обновлением версий элементов. В конце главы у нас появится почти окончательный вариант элемента First, в котором не будет лишь страниц свойств. Мы добавим их в следующей главе. А пока на время оставим элемент First и сосредоточим внимание на новом элементе. Наверное, он будет называться Second.
10.1 Проектирование элементов При проектировании элемента ActiveX необходимо принять во внимание один аспект, который заметно отличается от всего, что приходится учитывать при проектировании обычного приложения — конечно, речь идет о модульности (возможности его многократного использования). Поскольку элементы ActiveX изначально представляют собой программыкомпоненты, модульность становится важным критерием их дизайна. Иногда возможность многократного использования и аспекты компонентной работы того или иного элемента уходят на второй план, поскольку элемент разрабатывается для конкретной цели. Например, вы можете создать элемент, чтобы воспользоваться некоторыми возможностями элементов ActiveX (скажем, событиями). Или вы хотите написать большую часть кода на Microsoft Visual Basic, но какие-то фрагменты приходится писать на C++ или Java. Несомненно, подобные ситуации встречаются, но все же для большинства разработчиков элементов они нетипичны, поэтому основное внимание уделяется (и будет уделяться) компонентно-ориентированным элементам.
10.2 Визуальные и составные элементы Обязан ли элемент быть визуальным, то есть видимым на экране? Разумеется, нет, хотя невидимый элемент ActiveX не обязан быть элементом — например, его можно реализовать как сервер Automation с поддержкой событий (хотя после того, как появилось новое определение, такой объект будет считаться элементом). В ближайшие годы в программных инструментах (например, компиляторах C++) наверняка появятся средства для добавления событий в стандартные серверы Automation без накладных расходов, присущих серверам с редактированием на месте. Кроме того, появятся контейнеры, которые смогут принимать события от объектов, традиционно не считавшихся элементами. Все это приводит меня к заключению, что большинство «настоящих» элементов ActiveX будет визуальным.
www.books-shop.com
Обязан ли элемент ActiveX состоять всего из одного «элементарного» объекта (список, текстовое поле и т. д.)? Нет. Представьте себе элемент ActiveX, наделенный некоторыми функциями и обладающий собственным пользовательским интерфейсом, который предоставляется самим объектом. Хорошим примером может быть объект-«клиент», о котором я кратко упоминал в главе 1. Подобный объект спроектирован таким образом, что в него включается весь программный интерфейс для обращения к базе данных клиентов и весь пользовательский интерфейс, посредством которого можно работать с этой базой. Этот пользовательский интерфейс наверняка будет содержать множество отдельных компонентов пользовательского интерфейса, объединенных под одной крышей, — вполне допустимое применение для элемента ActiveX. Также интересно заметить, что пользовательский интерфейс, предоставленный элементом, может быть «заменен» пользовательским интерфейсом, предоставленным контейнером. Контейнер будет пользоваться тем же программным интерфейсом, но запретит элементу не отображать его пользовательский интерфейс. Зачем? Если создаваемые вами программы могут использоваться большим количеством компаний, работающих в одной области (например, страховании), нередко выясняется, что все компании хотят иметь один и тот же набор возможностей, однако пользовательский интерфейс должен быть в каждом случае разным. Некоторые компании захотят работать с вашим прекрасным, глубоко продуманным пользовательским интерфейсом. У других имеются внутренние стандарты, которые они желают сохранить (иногда с этим приходится ожесточенно спорить, потому что некоторые концепции пользовательских интерфейсов, которые мне приходилось видеть в компаниях, лишь усложняли работу с программами). Третьи захотят купить отдельный программный компонент и передать его независимой фирме, которая будет на его основе строить всю систему. Итак, если типичный элемент ActiveX отображается на экране, он должен быстро работать, не так ли? Большинство компонентов пользовательского интерфейса настолько опережает человеческую реакцию, что обычно мы обращаем внимание лишь на скорость прорисовки и визуальной обратной связи. «Скорость прорисовки» показывает, насколько быстро ваш элемент сможет изобразить себя на экране. Меня всегда поражало, как хорошо человеческий мозг умеет сравнивать. Иногда он пасует в оценке абсолютных величин («этот элемент рисуется слишком медленно»), но зато прекрасно справляется со сравнением («этот элемент рисуется значительно быстрее того»). Если ваш элемент окажется среди отстающих, он не выдержит конкуренции. Под «скоростью визуальной обратной связи» понимается промежуток времени, в течение которого внешний вид элемента изменяется в соответствии с действиями пользователя — например, щелчком мыши. Скажем, если кнопка тратит половину секунды на то, чтобы изобразить себя в нажатом состоянии, это вполне допустимо. Если задержка превысит секунду, она станет слишком заметной. Большинство визуальных оценок основано скорее на субъективных ощущениях, а не на реальности. Если ваш элемент кажется быстрым — значит, он действительно быстрый! Вот почему так популярны индикаторы прогресса («выполнено 56%… 57%… 58%» и т. д.); они создают иллюзию, будто программа работает быстрее. Представьте себе программу инсталляции, которая просто копирует файлы и никак не сообщает пользователю о происходящем. Возможно, она работает так же быстро, как и программа с индикатором прогресса, но внешне она выглядит более медленной. Даже разработчики компиляторов усвоили этот фокус — многие из них намеренно замедляют работу выводом текста лишь для того, чтобы вывести на экран количество откомпилированных строк. Эффект получается весьма заметным, и люди думают, что такой компилятор работает быстрее остальных. Следовательно, элемент должен уметь быстро выполнять основные графические операции. Сложные графические элементы должны немедленно обновлять основные части изображения (скажем, рисовать рамку диаграммы или условные обозначения) и дорисовывать остаток в фоновом режиме или при помощи аналогичного механизма. Конечно, из-за появления Web аспект скорости вышел на первый план. Все хотят пользоваться элементами, которые быстро загружаются (а следовательно, имеют малый размер) и ускоряют взаимодействие с Web-страницей, в которую они внедрены. Впрочем, последнее скорее обеспечивается не самим элементом, а программой-броузером. Microsoft предлагает решать эту задачу при помощи так называемой «прогрессивной пересылки», наиболее знакомым аспектом которой является асинхронная пересылка свойств. Тем не менее элементы тоже должны работать быстро, поскольку они могут использоваться при различных обстоятельствах. Если разместить большое количество элементов на Web-странице или экранной форме, время инициализации и скорость прорисовки каждого элемента становятся критически важными. Именно по этой причине
www.books-shop.com
спецификация OCX 96 в Microsoft была предложена командой разработчиков экранных форм — для элементов на формах исключительно важны показатели скорости и размера. Итак, при создании элемента следует в первую очередь стремиться к повышению скорости. Более того, если скорость становится главным фактором, вам придется тщательно выбирать язык программирования, библиотеку или инструмент для разработки элементов. Самые быстрые элементы обычно пишутся на C++ при помощи библиотеки ATL.
10.3 Объектная модель элемента При проектировании элемента необходимо тщательно продумать все, что относится к объектам Automation и вообще ко всем объектно-ориентированным аспектам системы. Какие свойства и методы должен раскрывать объект? Какие события он должен инициировать? Существует ли более низкий иерархический уровень, который также необходимо смоделировать? Например, объект, моделирующий клиента, может включать информацию об его адресе. Должна ли эта информация храниться как часть прочих данных объекта? Или же организовать ее в виде самостоятельного объекта, который умел бы проверять правильность своей информации, сохранять себя в базе данных и даже проверять, правильно ли указан почтовый индекс для конкретной страны? На основании чего должно приниматься решение? Необходимо продумать ответы на следующие вопросы:
Используется ли адрес другими компонентами системы? Должен ли адрес вести себя «интеллектуально»?
Если хотя бы на один вопрос будет дан положительный ответ, адрес вполне можно оформить в виде объекта. Если же положительными окажутся оба ответа, считайте, что у вас есть веские доводы в пользу такого решения. Далее необходимо продумать детали реализации и понять, как лучше оформить объект. Реализовать ли его в виде элемента или же оформить как сервер Automation? Вероятно, для обычного почтового адреса оба эти варианта окажутся «перебором» и будут работать слишком медленно (хотя реализация их в виде внутрипроцессного сервера может исправить положение). Следовательно, объект-адрес можно включить в объект-клиент и в последнее свойство Address, которое бы возвращало указатель на интерфейс программируемости объекта-адреса (то есть интерфейс, производный от IDispatch). В этом случае пользователи вашего элемента смогут писать на Visual Basic следующий код:
If TheCustomer.Address.IsValidZip = False Then MsgBox "The zip code is invalid - please re-enter" End If Чтобы включить объект-адрес в элемент, добавьте при помощи ClassWizard свойство Address типа LPIDISPATCH в класс, производный от COleControl. Затем снова воспользуйтесь ClassWizard и добавьте в проект новый класс, производный от CCmdTarget, для которого разрешена работа с Automation. Обычно этот класс не следует делать COM-создаваемым (то есть имеющим собственную фабрику класса и непосредственно создаваемым из клиентских приложений — таких, как Visual Basic), поскольку необходимо соблюдать иерархию объектов. Затем добавьте в новый класс свойства и методы объекта-адреса и реализуйте их. Если вы все же захотите сделать свой класс COM-создаваемым, необходимо соблюдать осторожность: элементы ActiveX не всегда ведут себя так же, как стандартные внутрипроцессные серверы Automation, и их необходимо правильно инициализировать. Создавая объект в элементе ActiveX без выполнения его кода инициализации, вы сами напрашиваетесь на неприятности. Не надейтесь, что вам всегда удастся сделать что-нибудь в таком роде:
Dim TheCustomer As Customer Set TheCustomer = CreateObject("Customer.Control.1") ...и т. д. Почему? Да потому что неявно происходящее здесь создание экземпляра методом CreateInstance не гарантирует правильной инициализации элемента (например, оно не создает клиентский узел,
www.books-shop.com
не загружает устойчивое состояние элемента и т. д.) Для простых элементов этот код может сработать, но рассчитывать на это не стоит. Если элемент больше похож на сервер Automation, подобный код обычно работает нормально. Мораль: знайте код вашего элемента и определяйте, что программист может сделать, а что — нет. Наверное, вам придется передавать какие-нибудь нестандартные структуры данных между экземплярами вашего элемента и контейнером. К сожалению, Automation ограничивает круг передаваемых типов, а возможные решения этой проблемы отнюдь не идеальны:
Создать отдельное свойство для каждого элемента структуры. Сделать это несложно, но при наличии большого количества элементов такой вариант оказывается крайне неэффективным. Упаковать все поля структуры в область памяти и передать ее в виде BSTR. Тем не менее этот вариант на редкость ненадежен, непонятен и чреват ошибками, особенно если какой-нибудь посредник на пути между элементом и контейнером захочет «интеллектуально обработать» такую строку и преобразует ее, скажем, из ASCII в Unicode. Не рекомендуется! Преобразовать все элементы структуры в текст и упаковать его в BSTR. Это решение тоже ненадежно и чревато ошибками — но к тому же оно медленнее работает! Не делайте этого. Передать данные через IDataObject. Такой вариант вполне может сработать. Конечно, контейнер должен знать, какие действия от него потребуются. Записать все в файл и передать имя файла. Хмм… надеюсь, эта бредовая идея даже не приходила вам в голову. Записать все в общую область памяти и передать ее имя. Однако при этом вы фактически делаете то же, что и интерфейс IDataObject.
Я уверен, что при желании можно придумать и другие варианты. Как правило, придется выбирать либо простоту программирования и простоту использования (отдельные свойства), либо эффективность и совместимость с контейнером (IDataObject).
10.4 Субклассирование элементов Нередко встречаются элементы, которые имитируют работу стандартных элементов Windows и дополняют их в том или ином отношении (например, специализированные текстовые поля для ввода дат или денежных сумм). Такие элементы следует проектировать особенно тщательно. В большинстве случаев следует использовать существующие возможности и субклассировать готовые элементы Windows. OLE ControlWizard содержит специальный флажок, облегчающий решение этой задачи. Тем не менее некоторые стандартные элементы (например, поля со списками) могут обладать недостатками и даже ошибками, из-за которых они не будут правильно перерисовываться в неактивном состоянии. В таком случае вам придется писать специализированный код для рисования элемента. Чаще всего это оказывается проще, чем создавать весь элемент заново.
10.5 Раскрывающиеся списки со значениями свойств Иногда возникает желание скопировать некоторые аспекты поведения элементов ActiveX в таких контейнерах, как Visual Basic. Например, элемент может сообщить контейнеру, какие значения допустимы для данного свойства, или даже присвоить имена значениям, отображаемым в раскрывающемся списке в окне свойств Visual Basic, как показано на рис. 10-1.
www.books-shop.com
Рис.10-1.Свойство с раскрывающимся списком значений и символическими именами На самом деле добиться этого очень просто, хотя интегрированная среда разработки (IDE) на этот раз никак не поможет. Основная работа делается вручную. К счастью, вам даже не придется писать новый код — все сводится к изменениям в библиотеке типов элемента. Для наглядности добавьте новое свойство в элемент First. Назовите его TestProp, реализуйте в виде переменной класса и удалите имя уведомляющей функции из текстового поля ClassWizard, чтобы эта функция не создавалась (для простоты). Назначьте свойству тип short. Перейдите к ODL-файлу элемента и вставьте следующие строки после директив importlib:
typedef enum { [helpstring("First")] valOne = 1, [helpstring("Second")] valTwo = 2, [helpstring("Third")] valThree = 3, } MYENUM; Этот фрагмент определяет в библиотеке новый тип данных MYENUM. Имя MYENUM выбрано произвольно — вы можете задать любое имя в соответствии с синтаксисом ODL/IDL. Этот тип данных фактически представляет собой обычный перечисляемый тип C/C++ (и, следовательно, относится к базовому типу short). Он содержит три величины: valOne, valTwo и valThree. Константам этого типа присвоены значения 1, 2 и 3. Если бы я этого не сделал, то они по умолчанию получили бы значения 0, 1 и 2. Кроме того, для каждой константы задана справочная строка. Содержащиеся в ней описания и должны присутствовать в окне свойств Visual Basic. Когда мы наделим свойство TestProp этой возможностью, Visual Basic будет отображать числовое значение вместе с текстовым описанием. Что же дальше? К счастью, ничего сложного. Просмотрите ODL-файл и найдите в нем запись для свойства TestProp. В настоящий момент она должна выглядеть так:
www.books-shop.com
[id(1)] short TestProp; (В вашем случае идентификатор может иметь другое значение.) Все, что от нас требуется — изменить тип свойства:
[id(1)] MYENUM TestProp; Это не вызовет никаких проблем, поскольку значения перечисляемых типов в C++ имеют тип short, так что такая замена не нарушит работу готового кода. Постройте новую версию элемента и поместите ее на форму Visual Basic. Затем в окне свойств найдите и попытайтесь изменить значение свойства TestProp. Как видите, свойство теперь ведет себя именно так, как мы хотели. Отсюда также можно сделать вывод, что Visual Basic пользуется библиотеками типов в большей степени, чем может показаться с первого взгляда. Эта простая схема не исчерпывает всех возможностей. Например, вы можете представлять значения свойств в виде строк, даже если контейнер не умеет преобразовывать «обычные» значения свойств в строки и наоборот. Например, если набор значений свойства не может быть легко представлен перечисляемым типом, описанный выше способ вам не подойдет. Вместо этого придется воспользоваться специальными функциями класса COleControl, которые соответствуют различным методам интерфейса IPerPropertyBrowsing. При помощи этого интерфейса элемент может назначить свойству имя, отличное от хранящегося в библиотеке типов (например, если имя свойства не является допустимым идентификатором C++ — This Property). Интерфейс IPerPropertyBrowsing рассматривается в главе 11, «Страницы свойств».
10.6 Работа с базами данных в элементах ActiveX Одно из усовершенствований, которые мне хотелось бы внести в элемент First, — более разумная схема поиска кодов. Оптимальный (во всяком случае, на мой взгляд) вариант заключается в том, чтобы работать с реальной базой данных, содержащей все коды, их символические имена и сообщения. Пользуясь средствами ODBC (или аналогичным механизмом, например Remote Data Objects), можно искать, добавлять и даже удалять HRESULT из базы. В моем коде возможность удаления отсутствует, но ее несложно реализовать самостоятельно. Для этого я воспользуюсь классом, производным от класса MFC CRecordSet, причем в большинстве мест это приведет лишь к упрощению кода. Листинги 10-1 и 10-2 содержат новый класс, производный от CRecordSet, который обеспечивает доступ к базе данных. В листингах 10-3 и 10-4 приведен обновленный главный файл элемента и его заголовочный файл (исходный текст элемента находится на прилагаемом CD-ROM в каталоге \CODE\CHAP10\FIRST).
ЗАМЕЧАНИЕ ODBC в настоящее время не поддерживает кодировку Unicode, поэтому вам не удастся построить обновленный элемент First для Unicode. Новая версия First сможет работать лишь в кодировке ANSI на платформах Microsoft Win32 до тех пор, пока не появится ODBC или другой, аналогичный механизм доступа к данным с поддержкой Unicode.
База данных создана в формате Microsoft Access (поэтому для работы с ней вам придется установить ODBC-драйвер для Microsoft Access, хотя сама СУБД Access не понадобится) и называется HRESULTS.MDB. В нее включена таблица, состоящая из трех столбцов: HRESULT типа long, символическое имя в виде текстового поля длиной до 255 символов, и текст сообщения в виде MEMO-поля (похожего на текстовое, за исключением того, что его максимальная длина может превышать 255 символов) длиной до 1023 символов. Сначала создается источник данных с именем «HRESULTS». Класс CDbSet был создан ClassWizard, я лишь внес в него несколько исправлений. Чтобы ClassWizard сгенерировал класс, нажмите кнопку Add Class, выберите из списка строку New… и введите имя класса CDbSet. Затем выберите базовый класс CRecordSet и нажмите кнопку Create. ClassWizard выводит список возможных источников данных ODBC (и DAO — Data Access Objects). Выберите из него добавленный ранее источник данных HRESULTS и нажмите кнопку OK. В источнике данных выводится список таблиц, входящих в базу. Он будет состоять всего из одной таблицы, которая тоже называется HRESULTS. После того как вы
www.books-shop.com
выберете его и нажмете кнопку OK, ClassWizard создает класс с тремя переменными, в которых хранятся значения всех трех столбцов. Когда класс будет создан, необходимо внести в него пару мелких исправлений, чтобы он работал в соответствии с требованиями элемента. Сначала необходимо отредактировать вызовы RFX_Text в функции DoFieldExchange и указать в них длину полей. Для этого достаточно добавить в каждый вызов четвертый параметр. Поле символического имени может иметь длину до 255 символов, поэтому в первый вызов RFX_Text добавляется параметр 256. Поле сообщения может иметь длину до 1023 символов, поэтому во второй вызов добавляется параметр 1024. Зачем это нужно? Когда набор записей (recordset) связывает свои переменные с полями источника данных, он передает адрес каждой из этих переменных через ODBC. Два текстовых поля реализованы как объекты класса CString. Когда объекту CString присваивается строка длиннее той, которая в нем содержится, под новую строку обычно выделяется другой буфер. Он почти наверняка будет располагаться по другому адресу, поэтому ODBC осуществляет связывание по неверному адресу, что приводит к самым ужасным и непредсказуемым последствиям. Отладочная версия библиотеки MFC перехватывает эту ситуацию директивой ASSERT (благодаря которой я и узнал о существовании этой проблемы!). Функции RFX_Text могут включать четвертый параметр, предназначенный именно для этой цели.
ЗАМЕЧАНИЕ На самом деле для поля символического имени четвертый параметр не нужен, поскольку по умолчанию ему все равно присваивается длина в 255 символов. Просто я стараюсь быть последовательным и облегчаю возможное изменение кода в будущем. Далее необходимо добавить параметр в класс набора записей. Этот параметр определяет значение, которое передается ODBC при выполнении запроса; с его помощью определяется нужная запись. Поскольку я знаю, что буду извлекать из базы по одной записи, подобное использование параметра оказывается гораздо более эффективным, чем извлечение всего набора и поиск в нем нужной записи. Пусть база данных поработает за нас — для этого она и нужна!
Параметр определяется в открытой (public) секции заголовочного файла:
long m_HRESULTParam; Параметр должен быть объявлен открытым, чтобы к нему можно было обратиться за пределами класса. Затем он используется в функции DoFieldExchange, сразу же после сгенерированного мастером фрагмента:
pFX -> SetFieldType(CFieldExchange::param); RFX_Long(pFX, "HRESULT", m_HRESULTParam); Данный фрагмент сообщает набору записей о том, что переменная является параметром, закрепленным за полем HRESULT. Как мы увидим при рассмотрении класса CDbSet, параметру m_HRESULTParam присваивается значение HRESULT, которое мы ищем в базе, и в дальнейшем база данных рассматривает его как составную часть передаваемого ей SQL-оператора. Наконец, осталось лишь увеличить количество параметров класса посредством увеличения переменной m_nParams в конструкторе. По значению этой переменной внутри класса определяется количество его параметров. Листинг 10-1. Заголовочный файл DBSET.H
// DbSet.h : заголовочный файл ////////////////////////////////////////////////////////////// // Набор записей CDbSet class CDbSet : public CRecordset { public: CDbSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CDbSet)
www.books-shop.com
// Данные полей/параметров //{{AFX_FIELD(CDbSet, CRecordset) long m_HRESULT; CString m_Symbol; CString m_Message; //}}AFX_FIELD // Переопределения // Переопределения виртуальных функций, // сгенерированные ClassWizard //{{AFX_VIRTUAL(CDbSet) public: virtual CString GetDefaultConnect(); // Строка соединения по умолчанию virtual CString GetDefaultSQL(); // SQL-оператор по умолчанию virtual void DoFieldExchange(CFieldExchange* pFX); //}}AFX_VIRTUAL
////////////////////////////////////////////////////////////// // Информация типа для элемента static const DWORD BASED_CODE _dwFirstOleMisc = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE | OLEMISC_RECOMPOSEONRESIZE; IMPLEMENT_OLECTLTYPE(CFirstCtrl, IDS_FIRST, _dwFirstOleMisc) ////////////////////////////////////////////////////////////// // CFirstCtrl::CFirstCtrlFactory::UpdateRegistry // Добавляет или удаляет элементы реестра для CFirstCtrl BOOL CFirstCtrl::CFirstCtrlFactory:: UpdateRegistry(BOOL bRegister) { // Убедитесь, что ваш элемент соответствует требованиям // совместной потоковой модели. Подробности приведены // в документе MFC TechNote 64. // Если элемент нарушает требования совместной модели, // необходимо модифицировать следующий фрагмент программы // и заменить 6-й параметр с afxRegApartmentThreading на 0.
CString csSql; csSql.LoadString(IDS_SQL); m_rsTable = new CDbSet; m_rsTable -> m_HRESULTParam = 0; if (m_rsTable -> Open(CRecordset::snapshot, csSql) == FALSE) { throw new CFirstException(CFirstException:: noDatabase); }
} catch (CException *e) { CString csExtra; UINT uStr = 0; if (e -> IsKindOf(RUNTIME_CLASS(CFirstException))) { ((CFirstException *)e) -> GetErrorString(uStr); } else if (e -> IsKindOf(RUNTIME_CLASS(CFileException))) { GetFileExceptionString((CFileException *)e, uStr); } else if (e -> IsKindOf(RUNTIME_CLASS(CDBException))) { csExtra = ((CDBException *)e) -> m_strStateNativeOrigin; } if (uStr) { csExtra.LoadString(uStr); } // Если текст сообщения не пустой, // занести его в окно сообщения if (!csExtra.IsEmpty()) { m_csBadMessage += _T("\n\nActual error message:\n\n"); m_csBadMessage += csExtra; } AfxMessageBox(m_csBadMessage, MB_OK);
}
}
// Сбросить строку с сообщением об ошибке m_csBadMessage.LoadString(IDS_BADMESSAGE); e -> Delete();
// Не перехватывает исключений, поэтому может инициировать // все, что инициируется в вызываемых ей функциях. // Сюда входят исключения CFirstException, // CFileException и CMemoryException. long CFirstCtrl::DoBatchLoad(CStdioFile *cfIn) { long lEntries = 0;
// Также передает любые исключения вызвавшей функции (может // передавать как минимум CFirstException, // CFileException и CMemoryException.) BOOL CFirstCtrl::GetNextDefineLine(CStdioFile *cfFile, CString *csLine, CString *csMessage) { _TCHAR szBuf[256]; CString csCompare; BOOL bFound = FALSE; LPTSTR lpszCnt; do { lpszCnt = cfFile -> ReadString(szBuf, 255); if (lpszCnt == NULL) { break; } csCompare = szBuf; bFound = (csCompare.Find(_T("// MessageText:")) != -1); } while (bFound == FALSE); if (bFound) { // Пропустить пустую строку комментария cfFile -> ReadString(szBuf, 255); // Получить строку (строки) сообщения csMessage -> Empty(); do { cfFile -> ReadString(szBuf, 255);
www.books-shop.com
if (szBuf[3]) { if (!csMessage -> IsEmpty()) { *csMessage += _T(" "); } szBuf[_tcslen(szBuf) - 1] = TCHAR(0); *csMessage += szBuf + 4; } } while (szBuf[3]); // Получить строку кода lpszCnt = cfFile -> ReadString(szBuf, 255); if (lpszCnt == NULL) { TRACE(_T("The file given to BatchLoad is in the wrong format\n")); throw new CFirstException(CFirstException:: badCodesFile); } *csLine = szBuf; return TRUE; } return FALSE; } long CFirstCtrl::GetTheCode(CString *csLine, CString *csSymbol) { // Если ‘#define’ отсутствует или находится // не в начале строки, файл не был создан утилитой MC if (csLine -> Find(_T("#define"))) { TRACE(_T( "#define line doesn’t start with exactly ‘#define’\n")); throw new CFirstException(CFirstException:: badCodesFile); } // Пропустить ‘#define’ int i = 7; // Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define’\n")); throw new CFirstException(CFirstException:: badCodesFile); } // Получить символическое имя csSymbol -> Empty(); while ((csLine -> GetLength() > i) && !(_istspace(csLine -> GetAt(i)))) { *csSymbol += csLine -> GetAt(i); ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define SYMBOL’\n"));
www.books-shop.com
}
throw new CFirstException(CFirstException:: badCodesFile);
// Пропустить символы-разделители while ((csLine -> GetLength() > i) && (_istspace(csLine -> GetAt(i)))) { ++i; } if (csLine -> GetLength() <= i) { TRACE(_T("#define line is only ‘#define SYMBOL’\n")); throw new CFirstException(CFirstException:: badCodesFile); } // В последних версиях файла WINERROR.H номер ошибки // может быть представлен в виде // _HRESULT_TYPEDEF(номер), // в этом случае следует пропустить имя макроса. int pos; if ((pos = csLine -> Find(_T("_HRESULT_TYPEDEF_("))) != -1) { i = pos + 18; // Длина имени макроса равна 18 }
}
// Получить номер CString csNumber; csNumber = csLine -> Mid(i); // Мы не сможем легко сообщить об ошибках, // которые происходят в этом месте (см. текст) return _tcstoul(csNumber, NULL, 0); void CFirstCtrl::OnFreezeEvents(BOOL bFreeze) { m_nEventsFrozen += (bFreeze ? 1 : -1); } // Не возбуждает исключений BOOL CFirstCtrl::CheckHResult(long nNewValue) { CString csLine; m_csMessage.Empty(); m_csSymbol.Empty(); m_bIsValid = FALSE; try {
case CFileException::generic: hr = CTL_E_ILLEGALFUNCTIONCALL; break; case CFileException::fileNotFound: hr = CTL_E_FILENOTFOUND; break; case CFileException::badPath: hr = CTL_E_PATHFILEACCESSERROR; break; case CFileException::tooManyOpenFiles: hr = CTL_E_TOOMANYFILES; break; case CFileException::invalidFile: hr = CTL_E_BADFILENAMEORNUMBER; break; case CFileException::directoryFull: case CFileException::diskFull: hr = CTL_E_DISKFULL; break; case CFileException::badSeek: case CFileException::hardIO: hr = CTL_E_DEVICEIOERROR; break; case CFileException::accessDenied: case CFileException::removeCurrentDir: case CFileException::sharingViolation: case CFileException::lockViolation: hr = CTL_E_PERMISSIONDENIED; break; case CFileException::endOfFile: hr = CTL_E_BADRECORDLENGTH; break; default: hr = CTL_E_ILLEGALFUNCTIONCALL; uStr = IDS_UNKNOWNEXCEPTIONCAUSE; break; }
} return hr; void CFirstCtrl::CheckDatabase() { if (m_rsTable == 0) { throw new CFirstException(CFirstException::noRecordSet); } if (!m_rsTable -> IsOpen()) { throw new CFirstException(CFirstException::dbClosed); } } Если вы внимательно просмотрели все изменения, внесенные в файл реализации, то наверняка заметили, что в целом файл стал выглядеть проще. Первое изменение находится в конструкторе элемента. Многие служебные переменные, которые инициализировались конструктором в старой
www.books-shop.com
версии, стали ненужными, поэтому я удалил эти переменные и все ссылки на них из определения класса. В число таких переменных вошли и недавно появившиеся структуры, в которых хранились добавленные к связанному списку значения — надобность в них отпала, поскольку база данных берет на себя все хлопоты по добавлению новых записей. Впрочем, появилась и одна новая переменная m_rsTable, которая является указателем на объект CDbSet. В ней будет храниться указатель на объект набора записей, используемого элементом. Этот объект создается несколькими строками ниже, внутри try-блока. Мы создаем объект оператором new, после чего присваиваем его внутренней переменной (параметру) значение 0. Перед тем как работать с набором записей, необходимо открыть его, поэтому мы вызываем функцию Open с двумя параметрами. Первый параметр функции Open сообщает базе данных, как именно должен быть открыт набор. Значение CRecordset::snapshot соответствует «моментальному снимку» базы данных на момент открытия, в нем не отражаются изменения, вносимые другими пользователями. Этот вариант нас вполне устраивает, поскольку никто не будет работать с базой данных одновременно с нами. При желании можете присвоить первому параметру значение CRecordset::dynaset, тогда все посторонние изменения будут отражаться в нашем наборе записей. Второй параметр функции Open представляет собой SQL-оператор, который будет определять критерий для заполнения нашего набора. SQL-оператор SELECT * FROM HRESULTS WHERE HRESULT=? хранится в файле ресурсов. Если вы не знакомы с языком SQL, перевожу: это означает, что мы хотим получить значения всех столбцов во всех записях таблицы HRESULTS, для которых значение в столбце HRESULT равно «?». Вопросительный знак не относится к синтаксису SQL, он будет заменен значением первого (и в данном случае единственного) параметра данного набора записей. Итак, если значение параметра равно 0, оператор принимает вид SELECT * FROM HRESULTS WHERE HRESULT=0. Я назначил столбец HRESULT первичным ключом базы данных. Это означает, что в ней не может быть двух записей с одинаковыми значениями HRESULT. Следовательно, набор записей всегда будет содержать не более одной записи. Если вызов Open окажется неудачным, инициируется исключение, которое вместе с остальными исключениями перехватывается catch-блоком. Он выглядит почти так же, как и в предыдущей версии, однако теперь в нем обрабатываются ошибки CDBException (класс исключений MFC, обусловленных работой с базами данных), а также присутствуют мелкие оптимизации. Обратите внимание на то, что для получения строки с описанием ошибки используется переменная m_strStatenativeOrigin класса CDBException. Эту строку библиотека MFC получает от ODBC, и она вовсе не обязательно будет простой и удобной! Деструктор элемента проверяет, была ли выделена память под набор записей, и если была — удаляет ее. Перед удалением деструктор также проверяет, открыт ли набор записей, и при необходимости закрывает его. Функция DoPropExchange почти не изменилась, из нее лишь исчезло присвоение переменной m_bInDispatch значения FALSE; причины объясняются в разделе «Сброс состояния элемента». Следующие изменения находятся в методе Add. Как и раньше, метод Add вызывает функцию FindEntry и прекращает работу, если значение HRESULT уже присутствует в базе данных. Осталась прежней и проверка переданных параметров. После этого все меняется. Метод Add сначала определяет, позволяет ли объект набора осуществлять добавление новых записей — если нет, он прекращает работу. Обратите внимание на то, что я работаю с переменной m_rsTable без предварительной проверки правильности объекта, на который она ссылается. На самом деле эта проверка осуществляется в функции FindEntry, которая рассматривается ниже. Если объект набора согласен принять данные, вызывается метод AddNew, который готовит его к получению новой записи. Далее мы задаем значения всех трех полей записи, пользуясь значениями, переданными Add, и вызываем функцию Update. Данные заносятся в базу — согласитесь, это гораздо проще, чем просматривать два файла, записывать смещения и т. д. Если новая запись была успешно добавлена, а текущее значение HRESULT помечено как недопустимое, мы вызываем функцию CheckResult и проверяем, не совпадет ли значение HRESULT новой записи с текущим.
www.books-shop.com
Аналогичные изменения произошли и в функции BatchLoad. Она вызывает новую функцию ChackDatabase, которая в свою очередь проверяет, существует ли набор, и открыт ли он в данный момент. Если хотя бы одно из этих условий не выполнено, CheckDatabase возбуждает исключение CFirstException. После возврата из CheckDatabase функция BatchLoad проверяет, можно ли добавить в базу новые записи, и пытается открыть входной файл в текстовом режиме для чтения. Если попытка окажется успешной, она, как и раньше, вызывает DoBatchLoad. Как и функция Add, BatchLoad в конце свой работы вызывает CheckHResult, если ей удалось успешно добавить хотя бы одну запись в базу, а текущее значение HRESULT является недопустимым. Функция FindEntry заметно упростилась и приобрела способность инициировать исключения. Она проверяет переменную m_rsTable, вызывая CheckDatabase, и присваивает параметру набора искомое значение HRESULT. Затем она вызывает метод Requery, который выполняет SQLоператор, и возвращает все записи базы данных, удовлетворяющие критерию поиска. В нашем случае результат поиска либо будет пустым, либо будет состоять всего из одной записи. Функция IsBOF проверяет, содержит ли полученный в результате выполнения запроса набор хотя бы одну запись — если он пуст, значение HRESULT отсутствует в базе данных. В противном случае набор содержит всего одну запись, которая является текущей, и мы можем получить значения ее полей. Именно это и делает функция GetInfo — заносит символическое имя и сообщение в переменные, принадлежащие классу элемента. Сначала она проверяет базу данных функцией CheckDatabase. Вообще говоря, это необязательно, потому что иначе мы просто не попали бы в функцию GetInfo. Однако я считаю подобные проверки хорошим стилем «защитного программирования» — решайте сами. В результате функция GetInfo может инициировать разнообразные исключения. Функция DoBatchLoad заметно упростилась по сравнению со своей предыдущей версией. Теперь ее основной цикл просто записывает значения из входного файла в базу данных при помощи последовательности AddNew/присвоение значений полей/Update. Функции GetNextDefineLine и GetTheCode не изменились, поскольку они работают с входным файлом, а не с базой данных. Функция OnFreezeEvents тоже осталась прежней, а функция OnEventAdvise вообще исчезла, потому что отпала необходимость в инициировании события FilesCreated. Это событие следует удалить (при помощи ClassWizard) из схемы событий элемента. В функции CheckHResult произошли незначительные изменения, обусловленные обработкой исключения CDBException. Данная функция использует функции более высокого уровня (например, FindEntry), поэтому изменения в механизме хранения/получения данных (переход от файловой подделки к настоящей базе с ODBC) никак не сказываются на ее работе. Новая функция CheckDatabase инициирует исключение, если значение переменной m_rsTable равно 0 (то есть попытка выделения памяти в конструкторе оказалась неудачной) или если указатель имеет правильное значение, но ссылается на закрытый набор записей. Неудачное открытие набора скорее всего свидетельствует о неверной установке источника данных в ODBC или об отсутствии файла, содержащего базу данных. Обратите внимание на то, что у функции DoError появился «двойник». Вторая версия DoError получает в качестве второго параметра строку с описанием ошибки (ссылку на CString) и передает ее при последующем вызове ThrowError или FireError. Первая версия DoError просто получает описание ошибки из строковой таблицы и затем вызывает вторую версию. Все старые вызовы DoError благополучно продолжают работать, но зато вторую версию DoError можно вызвать и напрямую при наличии готовой строки с описанием ошибки (как это делается при обработке исключений CDBException).
ЗАМЕЧАНИЕ Я добавил в CFirstException новые коды причины, приведенные в FIRSTEX.H и FIRSTEX.CPP. Хотя многие из кодов устарели и не используются в новой версии, я все же оставил их в файле. Кроме того, я сохранил соответствующие строки сообщений в файле ресурсов элемента. Удалять их необязательно, однако это можно сделать для того, чтобы уменьшить элемент.
10.7 Сброс состояния элемента
www.books-shop.com
В первом издании этой книги я включил в элемент новый метод Reset, который вызывал функцию OnResetState. Вся работа функции OnResetState нашего элемента сводится к вызову функции базового класса, которая, в свою очередь, просто вызывает DoPropExchange для того, чтобы присвоить всем свойствам стандартные значения (которые берутся из хранилища, на базе которого создавался элемент, а при его отсутствии каждому свойству присваивается значение по умолчанию). Отсюда становится ясно, почему из функции DoPropExchange пропало присвоение m_bInDispatch. В более ранних версиях эта функция могла вызываться только вне вызова метода или обращения к свойству, и, следовательно, любые возникающие исключения должны были преобразовываться в события. Теперь она может вызываться и при выполнении метода Reset, поэтому ее пришлось изменить. Я не включил этот метод во второе издание, потому что он слишком редко использовался.
ПРЕДУПРЕЖДЕНИЕ Сброс элемента — важное действие, которое может происходить в самые разные моменты. Не забывайте во время сброса перевести в исходное состояние все переменные класса, глобальные переменные и т. д., чтобы состояние элемента было определенным и устойчивым. Если забыть об этом, несоответствия в значениях переменных могут привести к нарушению работы элемента.
10.8 Отладка элемента Как же происходит отладка элементов ActiveX? В 16-разрядной среде отладка COM-серверов, оформленных в виде EXE-файлов, была на редкость сложным и противным занятием. 32разрядный отладчик заметно упрощает отладку 32-разрядных серверов для Microsoft Windows NT или Windows 95. К счастью, элементы ActiveX почти всегда реализуются в виде внутрипроцессных серверов, а это означает, что они оформляются в виде DLL и принадлежат адресному пространству своего контейнера. Благодаря этому обстоятельству отладка происходит намного проще. Начинать следует с выбора выполняемого файла, который должен запускаться на время сеанса отладки. Обычно таким файлом является тестовый контейнер, поскольку с его помощью можно опробовать большую часть возможностей элемента и перехватить возникающие ошибки. Выполните команду Build|Settings в Microsoft Visual C++ версии 4.x. Перейдите на вкладку Debug, выберите категорию General и укажите в поле Executable For Debug Session путь и полное имя файла тестового контейнера. Полное имя должно включать расширение файла — например, C:\MSDEV\BIN\TSTCON32.EXE. После этого запустите тестовый контейнер кнопкой Go на панели инструментов проекта. Чтобы приступить к отладке элемента, необходимо вставить его в запущенную отладчиком копию тестового контейнера. Вы можете расставить точки прерывания в элементе до или после его загрузки. Например, при отладке конструктора стоит сделать это заранее. С другой стороны, если вы отлаживаете метод, точки прерывания стоит задать после загрузки элемента. Если теперь в элементе будет выполнена строка, содержащая точку прерывания, или возникнет ошибка, которая заставит отладчик прервать работу (например, сработает директива ASSERT), программа останавливается, а в отладчике выводится текущая строка. Вы можете просмотреть локальные переменные, узнать содержимое области памяти, присвоить переменным новые значения и вообще сделать все, что обычно делается во время сеанса отладки.
10.9 Версии элемента Давайте представим себе, что вы разработали элемент — скажем, First. Ваше творение имеет несколько устойчивых свойств, из которых лишь одно является его собственным (свойство HResult). Предположим, вам удалось продать множество копий этого элемента (при достаточно живом воображении можно представить и такое).
www.books-shop.com
Проходит некоторое время, и вы отчетливо понимаете, что можете усовершенствовать свой элемент и заработать еще больше — нужно лишь сделать x, y и z. При этом совершенно неважно, что делают эти x, y и z (если придумаете, сообщите мне), — главное, что в новой версии элемента появляется дополнительное свойство NewProp. Пользователи получают новую версию элемента и надеются, что она будет полностью совместима с предыдущим вариантом. Другими словами, они смогут просто записать новый файл элемента на место старого, и все программы будут работать с ним так же хорошо, как и со старым. Однако при этом возникает законный вопрос: «Каким образом новый элемент сможет читать (и, возможно, записывать) значения свойств, сохраненные старой версией, и при этом присваивать правильное значение новому свойству?» Ответ кроется в функции DoPropExchange. Просмотрите эту функцию, и вы увидите, что после вызова ExchangeVersion она подтверждает восстановленное значение свойства HResult, если функция DoPropExchange была вызвана в ответ на требование «загрузить значения свойств». Проверка версии выполняется аналогичным образом. Обычно ExchangeVersion сохраняет номер версии элемента среди его свойств. Разумеется, восстановленный номер версии в таком случае относится к сохраненным свойствам, а не к работающему элементу. Следовательно, элемент версии 2.0 может обнаружить, что он загружает свойства версии 1.0, и присвоить новым, отсутствующим в старой версии свойствам значения по умолчанию. При сохранении свойств вы можете выбрать, для какой версии они должны сохраняться — 1.0 или 2.0. Какую бы версию вы ни выбрали, необходимо приказать ExchangeVersion сохранить правильный номер версии (опять же речь идет о версии, относящейся к сохраненным свойствам, а не к текущей версии работающего элемента). При создании элемента при помощи OLE ClassWizard создаются две глобальные переменные, которые инициализируются значениями основного и дополнительного номера версии. В случае смены версии элемента необходимо изменить эти переменные и затем использовать их при вызове DoPropExchange. Итак, для того чтобы функция DoPropExchange гипотетической версии 2.0 элемента First правильно работала со свойствами версии 1.0, необходимо сделать следующее:
void CFirstCtrl::DoPropExchange(CPropExchange* pPX) { ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor)); COleControl::DoPropExchange(pPX); PX_Long(pPX, _T("HResult"), m_HResult, 0); if (pPX -> IsLoading()) { ReallySetHResult(m_HResult); } // Версия 2.0 или выше? If (pPX -> GetVersion() >= MAKELONG(0, 2)) { PX_Long(pPX, _T("NewProp"), 1234); } } В этом фрагменте мы проверяем номер версии для загруженных свойств и загружаем новое свойство NewProp в том случае, если мы имеем дело с версией 2.0 и выше. Кроме того, в новой версии некоторые свойства могут исчезать. Например, в версии 3.0 нашего элемента значение NewProp может оказаться ненужным. Хотя Microsoft не рекомендует удалять свойства в новых версиях элемента (вместо этого рекомендуется все равно сохранять и загружать эти свойства, игнорируя их значения в новых версиях), это «всего лишь программа», поэтому вы вправе делать все, что считаете нужным. Разумеется, весь этот раздел относился в первую очередь к элементам на базе MFC. Тем не менее все сказанное справедливо и для элементов, написанных при помощи других средств. Отличие состоит лишь в том, что на этот раз вам придется самостоятельно писать код для сохранения, загрузки и сравнения сохраненного номера версии с текущим.
www.books-shop.com
10.10 Справочные файлы для элементов Хотя последний раздел этой главы занимает немного места, он посвящен достаточно важной теме — справочным файлам, которые упрощают работу с элементами и способствуют их коммерческому успеху. Справка может использоваться в различных ситуациях. Наиболее очевидная из них — отображение информации по имени справочного файла и идентификатору контекста, передаваемым при возникновении ошибки. Если ваш элемент отличается особой сложностью, будет полезно составить справку для его пользователей (а не для программистов). Возможно, в нее следует включить общее описание, а также отдельные справочные разделы для конкретных компонентов диалоговых окон, свойств, методов или событий элемента. Если учесть, что с готовыми элементами обычно работают две категории людей — разработчики и пользователи, будет непросто выдержать нужный баланс при составлении справки. Иногда вместо вывода справки необходимо переложить всю ответственность на приложение-контейнер. Вывод справочной информации в элементах на базе MFC организован достаточно просто. При установке флажка Context Sensitive Help в OLE ControlWizard не происходит ничего особенного, разве что появляется новый подкаталог HLP, содержащий базовый HLP-файл в формате RTF (Rich Text Format, стандартный для «исходных текстов» справочных файлов), файл с расширением HPJ и пакетный файл MAKEHELP. HPJ-файл представляет собой файл проекта для компилятора справки, он управляет работой Windows Help Compiler (HC31.EXE). Пакетный файл MAKEHELP.BAT преобразует идентификаторы ресурсов, используемые вашим элементом, в подключаемый (include) файл для компилятора справки. Для этого используется MAKEHM.EXE — утилита для работы со справочными файлами, входящая в комплект Visual C++. Затем MAKEHELP.BAT запускает компилятор справки. Чтобы обеспечить вызов справки, вам придется дополнительно включить в элемент ряд программных перехватчиков (hooks).
ЗАМЕЧАНИЕ В Windows 95, а также в Windows NT версий 3.51 и выше используется более современный формат справки, обладающий расширенными возможностями по сравнению с форматомWindows 3.1. Он создается новым компилятором справки HCW.EXE. Тем не менее новый формат полностью совместим со старым, созданным при помощи HC31, хотя некоторые новые возможности оказываются недоступными для последнего. Чтобы воспользоваться этими возможностями, вам придется преобразовать справочный файл, созданный OLE ControlWizard, в формат HCW.
Наиболее очевидный перехватчик должен реагировать на нажатие пользователем клавиши F1. Чтобы создать его, воспользуйтесь ClassWizard и включите обработчик сообщения WM_KEYDOWN в главное окно элемента, то есть в класс, производный от COleControl (если это не было сделано ранее). Затем включите аналогичный обработчик во все оконные классы элемента, которые должны обрабатывать нажатия F1. Обработчик должен определять код нажатой клавиши, и при нажатии F1 вызывать функцию CWinApp::WinHelp. Типичный пример может выглядеть так:
void CConvolveCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar == VK_F1) { AfxGetApp -> WinHelp(0, HELP_CONTENTS); } else { COleControl::OnKeyDown(nChar, nRepCnt, nFlags); } } Функция AfxGetApp возвращает указатель на объект-приложение элемента, производный от CWinApp. В нашем примере этот указатель используется для вызова функции WinHelp, отображающей страницу содержания (contents) заданного справочного файла. При желании можно усложнить задачу и организовать контекстную справку. В этом случае нажатие клавиш Shift+F1 переводит элемент в специальный режим, при котором щелчок мыши на различных
экранных объектах будет выводить справочную информацию о них. Если вам захочется рассмотреть пример кода, в котором реализована данная возможность, создайте стандартное приложение AppWizard с поддержкой контекстной справки. Имя справочного файла хранится в переменной m_pszHelpFilePath объекта-приложения. По умолчанию справочный файл имеет тот же путь и имя, что и сам элемент, но его расширение заменяется на HLP.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Справку можно вывести и для страницы свойств. Поскольку о страницах свойств рассказывается лишь в следующей главе, я не стану опережать события — сначала нужно хотя бы знать, как работать со страницами свойств! В следующей главе имеется небольшой раздел, посвященный работе со справкой в страницах свойств.
Даже если ваш элемент создавался другим способом, основные принципы работы со справкой остаются прежними. Если вы не считаете себя асом по составлению справок (например, я к этой категории не отношусь!), то всегда можете прибегнуть к услугами MFC OLE ControlWizard и сгенерировать «скелет» справочных файлов.
www.books-shop.com
Глава
11
Страницы свойств Позади осталось больше половины книги. Возникает вопрос: почему я до сих пор почти ничего не сказал о том пользовательском интерфейсе, который элементы предоставляют для работы со свойствами? Хотя простые и удобные страницы свойств чрезвычайно важны для интеграции элемента с остальными компонентами вашей системы, я все же считаю их чем-то второстепенными и почти всегда откладываю на конец работы над проектом. Вредная привычка? Возможно, но не торопитесь осуждать меня и сначала учтите следующее. Страница свойств обязательно присутствует в каждом элементе, созданном OLE ControlWizard (если только вы намеренно не удалили ее). Загрузите любой из созданных ранее элементов в тестовый контейнер и вызовите команду OLE Properties. На экране появляется стандартная страница свойств, создаваемая OLE ControlWizard для каждого элемента. Она представляет собой абсолютно пустую вкладку General в стандартном диалоговом окне.
ЗАМЕЧАНИЕ Команду OLE Properties можно выполнить несколькими различными способами. Проще всего в тестовом контейнере подвести курсор мыши к краю элемента, и, когда он примет вид стрелок, указывающих в четырех направлениях, щелкнуть кнопкой мыши. Открывается контекстное меню, содержащее всего одну команду — Properties. Вам остается лишь выполнить ее.
11.1 Что такое страницы свойств? На момент первого издания этой книги наличие страниц свойств считалось едва ли не обязательным требованием для каждого элемента. Затем наступила эпоха Internet, и ситуация резко изменилась. Большинство пользователей элементов теперь составляют не программисты, а простые смертные, которые загружают HTML-страницы, содержащие элементы, с какого-нибудь Web-сервера. Вряд ли этим пользователям захочется иметь дело со страницами свойств отдельных элементов — скорее всего, они предпочтут работать с HTML-страницей в целом. Создатель элемента в эту схему вообще не вписывается, если только он не является заодно и автором HTML-страницы. Что же это означает для страниц свойств? Как минимум то, что вам следует дважды подумать, прежде чем включать в свой элемент страницы свойств. Давайте почитаем, что я писал о них в первом издании. На вопрос, что же такое «страница свойств», можно ответить просто: это пользовательский интерфейс, определяемый элементом [ActiveX] для непосредственной работы со свойствами элемента, не требующей вмешательства со стороны контейнера. Впрочем, можно дать более сложный ответ, который отчасти объясняет, почему страницам свойств придается такое большое значение. Каждый элемент [ActiveX] способен приносить пользу лишь внутри некоторого контейнера. Любой контейнер, умеющий работать с элементом [ActiveX], почти наверняка предоставит средства для обращения к свойствам элемента и присвоения им нужных значений. Большинство контейнеров позволяет делать это в режиме конструирования. Некоторые контейнеры предоставляют подобную возможность и в режиме выполнения (доступ осуществляется на программном уровне). Механизм, при помощи которого это делается, специфичен для данного контейнера — во всяком случае, никаких стандартов в этой области пока нет. Впрочем, такие стандарты вряд ли принесли бы какую-нибудь пользу; контейнеры работают по-разному и поддерживают различные парадигмы пользовательского
www.books-shop.com
интерфейса — вряд ли стоит ограничивать творческие порывы разработчиков и указывать им, что некую задачу нужно выполнять именно так, а не иначе. К тому же для этого должна существовать некая личность или организация, которая обладает полномочиями для подобных распоряжений (лично я сомневаюсь, что кто-нибудь захочет взять на себя подобную ответственность). Тем не менее свойства некоторых элементов необходимо просматривать и задавать прямо в режиме выполнения, без вмешательства на программном уровне. Кроме того, со временем появятся элементы, которые будут представлять собой части операционных систем, интеграция которых с системными компонентами потребует наличия пользовательского интерфейса для работы со свойствами. Переход на новый пользовательский интерфейс в Windows 95 и поздних версиях Windows NT фактически определил стандарты для страниц свойств объектов, не зависящих от типа самих объектов. Предполагается, что в этой среде каждый объект обладает страницами свойств, которые пользователь может вызвать щелчком правой кнопки мыши. Механизм страниц свойств стал общепризнанным атрибутом элементов [ActiveX]. Никто не приказывает вам включать страницы свойств в создаваемые элементы, но если вы этого не сделаете, то окажетесь в явном меньшинстве. MFC настолько облегчает процесс добавления страниц свойств в элементы, что вам почти не придется тратить никаких усилий на их поддержку. В общем все сказанное около года назад остается правдой, за единственным исключением — среднестатистический элемент теперь гораздо чаще работает в режиме выполнения, а не в режиме конструирования, так что любой пользовательский интерфейс, предоставляемый контейнером, будет использоваться реже, чем я предсказывал. Конечно, страницы свойств и связанный с ними код занимают драгоценное место в элементе, соответственно, увеличивается время его пересылки, соответственно, покупатели будут выбирать элементы, разработанные конкурентами, соответственно, вам придется выкинуть за борт все, без чего можно обойтись, соответственно, поддержка страниц свойств должна включаться далеко не в каждый элемент. Еще год назад дело обстояло иначе. Примером «нового мышления» могут послужить элементы, сгенерированные ATL 2.0, — по умолчанию страница свойств в них не включается. Тем не менее многие разработчики элементов ActiveX все же стремятся побольше узнать о страницах свойств, чтобы принять обоснованное решение — должен ли тот или иной элемент содержать страницы свойств. Возможно, будущие элементы ActiveX будут содержать физически различные компоненты режима конструирования и режима выполнения, так что страницы свойств не утратят своей актуальности, но будут присутствовать лишь в виде DLL для режима конструирования. В соответствии с текущей парадигмой пользовательского интерфейса, «страницы свойств» представляют собой взаимосвязанные диалоговые панели, отображаемые на разных вкладках диалогового окна. На рис. 11-1 изображен типичный пример страницы свойств — этот набор страниц мы реализуем для элемента First по мере знакомства с материалом этой главы (исходный текст элемента находится на прилагаемом CD-ROM, в каталоге \CODE\CHAP11\FIRST). На самом деле страницы свойств представляют собой нечто большее, нежели простой набор вкладок диалогового окна. Каждая страница свойств является самостоятельным COM-объектом. Например, она поддерживает интерфейсы для своего создания и для передачи фокуса определенному элементу на определенной странице. Некоторые стандартные страницы свойств (для цветовых, шрифтовых и графических объектов) реализованы на системном уровне, они находятся в DLL-библиотеке OLEPRO32.DLL. В рекомендациях пользовательского интерфейса Microsoft Windows 95 приведены желательные размеры страниц свойств (на данный момент их два), и в отладочных версиях элементов MFC выдает предупреждающее окно сообщения при попытке отобразить страницу свойств, размер которой не совпадает с одним из этих стандартов. Сообщение является скорее информационным, нежели обязательным для исполнения, поскольку иногда элементам все же приходится выводить страницы свойств нестандартного размера. При создании элемента с помощью Microsoft Visual C++ OLE Control Wizard (то есть на базе MFC) мастер по умолчанию создает пустую страницу свойств. Вы можете добавить к ней элементы, создать новые страницы и связать элементы на страницах с конкретными свойствами элемента. Вы даже можете воспользоваться стандартными функциями DDX и DDV библиотеки MFC, чтобы организовать пересылку данных между страницами и переменными и проверить, лежат ли введенные значения в допустимом диапазоне. Функции MFC по работе со страницами свойств заключены внутри класса COlePropertyPage, который является базовым для создаваемых
www.books-shop.com
ControlWizard и Class Wizard специализированных классов-оболочек, работающих со страницами свойств вашего элемента.
Рис.11-1.Страницы свойств элемента First
11.2 Как работать со страницами свойств Страницы свойств легко реализуются и просты в использовании. Редко когда от страницы свойств могут потребоваться возможности, отсутствующие в стандартном диалоговом окне. Для наглядной демонстрации работы со страницами свойств мы возьмем элемент First из предыдущей главы и изменим его страницу свойств, чтобы в ней отображалось значение свойства HResult и его можно было изменить. Первое, что необходимо сделать перед добавлением свойства — решить, элемент какого типа будет представлять его на странице. Иногда выбор очевиден — например, флажок для логического свойства или текстовое поле для текстового свойства. В других случаях принять решение оказывается сложнее. Например, как отображать свойство для цвета — в виде шестнадцатеричного числа, десятичного числа со знаком или набора цветных кнопок? (Вот почему для цветовых страниц свойств существуют стандартные реализации!) Свойство HResult логичнее всего представить в виде текстового поля, однако его значение должно быть числовым. Соответствующая функция DDX решает эту проблему — она преобразует введенный текст в длинное целое число, которое сохраняется в элементе. Если вы хотите осуществлять проверку диапазона (в данном случае она не нужна), можно воспользоваться DDVфункциями библиотеки MFC. Алгоритм включения свойства HResult в страницу свойств выглядит следующим образом: 1.
При помощи редактора ресурсов отредактируйте ресурс диалогового окна IDD_PROPPAGE_FIRST и добавьте в него надпись (label) с текстом &HResult: (символ & определяет мнемонический символ в диалоговом окне, во время выполнения программы он не отображается, а буква H в слове HResult подчеркивается). Если быть совсем точным, начинать необходимо с удаления из диалогового окна надписи TODO, занесенной туда мастером. Лично меня это сильно раздражает. 2. Разместите текстовое поле рядом с надписью. 3. Вызовите ClassWizard сочетанием клавиш Ctrl+W. Перейдите на вкладку MemberVariables и выделите класс CFirstPropPage. 4. В списке элемента должен присутствовать идентификатор IDC_EDIT1, который соответствует добавленному текстовому полю. Нажмите кнопку Add Variable. 5. Введите имя переменной — например, m_HResult. 6. Убедитесь в том, что в раскрывающемся списке Category выбрана строка Value, присвойте новой переменной тип long. 7. Введите в текстовое поле OLE Property Name значение HResult.
www.books-shop.com
8. 9.
Закройте диалоговое окно Add Member Variable и окно ClassWizard, нажав в каждом из них кнопку OK. Заново постройте элемент.
Как видите, программирования пока нет и в помине! Этапы 3 и 4 можно объединить — удерживайте нажатой клавишу Ctrl и сделайте двойной щелчок на текстовом поле в редакторе диалоговых окон, на экране сразу появляется диалоговое окно ClassWizard Add Member Variable для класса указанного элемента диалогового окна. Если теперь запустить элемент в тестовом контейнере и вызвать страницу свойств (например, подвести курсор к краю элемента, чтобы из обычной стрелки он превратился в крестик, щелкнуть правой кнопкой мыши и выполнить команду Properties из контекстного меню), вы увидите нечто похожее на рис. 11-2. Попробуйте ввести новое значение HResult и нажмите кнопку OK, чтобы убрать с экрана страницу свойств, — вы увидите, как элемент изменится в соответствии с новым значением заданного свойства.
Рис.11-2. Предварительный вариант страницы свойств элемента First На странице свойств присутствует еще одна кнопка — Apply. Она блокируется (то есть является недоступной) до тех пор, пока на странице не будет изменено хотя бы одно свойство. При нажатии разблокированной кнопки Apply свойствам элемента немедленно присваиваются значения из страниц свойств — вам не приходится закрывать диалоговое окно Properties.
ЗАМЕЧАНИЕ Поскольку наша страница обновляет свойство элемента посредством стандартного механизма Automation, присвоение свойству недопустимого значения HRESULT приведет к инициированию события InvalidHResult. Страницы свойств не пользуются никаким волшебством — они работают с теми же механизмами, как и другие средства. Это и позволяет включать стандартные страницы свойств в любые элементы.
11.3 Проектирование страниц свойств Ирония судьбы — похоже, мне придется давать вам советы по поводу дизайна пользовательского интерфейса. Как вы могли понять из моих предыдущих замечаний, к подобным советам следует относиться с некоторой долей скепсиса. Разумеется, дизайн пользовательского интерфейса все же подчиняется некоторым базовым концепциям и общим положениям, о которых можно узнать из любой книжки, посвященной дизайну графического пользовательского интерфейса (GUI) или интерфейса «человек-компьютер»(HCI).
www.books-shop.com
Я же ограничусь рассмотрением того, что в них стоит делать, а что — нет. Первым делом мне вспоминаются кошмарные «диалоговые окна из преисподней». Громадное количество вкладок в таких окнах угнетает пользователя, он не может понять, где искать то или иное свойство. На мой взгляд, этот недостаток присущ некоторым коммерческим элементам (один из которых, кстати, был создан в Microsoft). Решение проблемы состоит в том, чтобы придерживаться золотого правила пользовательских интерфейсов (да, наверное, и всего остального в этой жизни) — будьте проще. Как бы банально ни звучало это утверждение, оно остается справедливым, поскольку хорошо отражает потребности пользователя при общении с компьютером. Стоит ли отображать все свойства элемента на странице? На этот вопрос можете ответить только вы, проектировщик элемента. Некоторые свойства могут оказаться настолько непонятными для пользователя, что их можно сразу отбросить. Другая интересная возможность заключается в объединении нескольких свойств в одно составное значение. Это происходит не так часто, однако мне приходилось видеть нечто подобное в элементе для страховой компании, о котором я упоминаю время от времени. Данный элемент содержал три свойства, относящихся к клиенту, — имя, отчество и фамилию. Тем не менее страница свойств была спроектирована так, что за счет конкатенации всех трех свойств имя отображалось в ней в виде одной строки. Подобную возможность тоже следует оценивать применительно к конкретному случаю — обычно приходится выбирать между удобством использования и усложнением программирования. Мне приходилось слышать, что в таких ситуациях предпочтение всегда должно отдаваться удобству. Я придерживаюсь более практичной точки зрения: главное, чтобы ваши компромиссные решения нормально работали, и вы получите полезные и удобные элементы — и к тому же их будет легче модифицировать в будущем! Также следует подумать о том, на какую аудиторию рассчитаны страницы свойств вашего элемента ActiveX. Например, элемент может быть предназначен только для программистов или для лиц, знакомых с тонкостями банковских инвестиций, и т.д. Следовательно, страница свойств всегда должна ориентироваться на основную категорию пользователей, а не на абстрактного пользователя. Страницам свойств следует присваивать один из двух стандартных размеров — 250ґ62 или 250ґ100 единиц измерения диалоговых окон. Отладочная версия MFC при первом отображении страницы свойств, не соответствующей этим размерам, выводит предупреждающее сообщение (хотя не предпринимает никаких дальнейших действий). В окончательной версии сообщение не выводится. Наличие стандартных размеров вовсе не означает, что вы не сможете выбрать другой размер, но в этом случается ваша страница будет отличаться от страниц, отображаемых другими элементами, или от встроенных страниц свойств для цветов, шрифтов и графики. По возможности старайтесь соблюдать стандартные размеры. Если вы используете DDV-функции библиотеки MFC для проверки содержимого полей на странице, то они будут вызываться лишь при нажатии кнопки OK или Apply. Пользователи обычно предпочитают, чтобы ошибки обнаруживались непосредственно во время ввода данных. Обычно для этого содержимое поля проверяется перед тем, как передавать фокус другому полю. Если между полями существует логическая связь (например, некоторые свойства имеют смысл лишь в контексте других), постарайтесь сделать этот факт очевидным для пользователя и выполняйте соответствующие визуальные действия — например, блокируйте поля, присваивайте им новые значения и т. д. В основном эти рекомендации продиктованы здравым смыслом, и их главная цель — по возможности упростить жизнь пользователя страницы свойств. Если страница свойств вам чем-то не понравится, то почти наверняка она не понравится и всем остальным пользователям. Внесите необходимые изменения!
Отображение свойств, доступных только для чтения Элемент First содержит немало свойств, доступных только для чтения, — их значения не могут изменяться непосредственно программистом или пользователем, а меняются лишь вследствие изменения свойства HResult. Стоит ли отображать такие свойства на странице? Если вы (как и я) предпочитаете видеть сразу все свойства — тогда отображайте. Конечно, сделать это несложно. Сделайте все, что вы делали при добавлении свойства HResult, но с одним дополнительным этапом — пометьте каждое текстовое поле как доступное только для чтения и заблокированное.
www.books-shop.com
Для этого сделайте двойной щелчок на текстовом поле — открывается диалоговое окно Edit Properties. Флажок Disabled находится на вкладке General, а флажок Read-Only — на вкладке Styles. Тем самым вы разрешаете отображение свойств на странице, но не их изменения. Страница свойств при этом позволяет одновременно выводить относительно большое количество значений. На рис. 11-3 изображена спроектированная мной страница свойств для элемента First (не нравится — переделайте). И еще одно немаловажное замечание — страницы свойств иногда могут вызываться сразу для нескольких элементов. Для проектировщика страницы такая ситуация не вызывает особых трудностей, просто она означает, что, например, заданный в странице шрифт должен быть применен сразу к нескольким элементам.
Рис.11-3.Страница свойств элемента First со свойствами, доступными только для чтения
11.5 Дополнительные страницы свойств В некоторых ситуациях свойства элемента приходится отображать на нескольких страницах. Например, для некоторых свойств (цветовых, шрифтовых и графических — см. следующий раздел «Стандартные страницы свойств») существуют готовые страницы. С другой стороны, элемент может просто иметь слишком много свойств, которые не помещаются на одной странице. Тем не менее всегда старайтесь свести количество страниц к минимуму, не загромождайте страницы и держите логически связанные свойства на одной странице (иногда для этого приходится проявить чудеса изобретательности!). Чтобы добавить новую страницу свойств, необходимо создать новое диалоговое окно и затем — новый класс для работы с ним. Делается это так: 1.
Создайте новое диалоговое окно командой Insert|Resource и выберите подходящий тип окна (прекрасно подойдет IDD_OLE_PROPPAGE_LARGE или IDD_OLE_PROPPAGE_SMALL). Переименуйте его — например, IDD_PROPPGE2_FIRST. 2. Удалите надпись TODO (так «любезно» размещенную в окне), для чего выделите ее и нажмите клавишу Delete. 3. Откройте диалоговое окно Dialog Properties, для чего щелкните правой кнопкой мыши на странице свойств и выполните команду Properties изконтекстного меню. Убедитесь в том, чтобы на вкладке Styles были установлены свойства страницы Child (список Style) и None (список Border), а все прочие флажки оставались снятыми или заблокированными (особенно флажки Titlebar и Visible). 4. Задайте нужный размер диалогового окна; не забывайте о важности стандартных размеров (диалоговые окна IDD_OLE_PROPPAGE_LARGE и IDD_OLE_PROPPAGE_SMALL изначально обладают стандартными размерами). 5. Вызовите ClassWizard сочетанием клавиш Ctrl+W. 6. Открывается окно диалога Adding A Class MFC ClassWizard. Установите переключатель Create A New Class и нажмите кнопку OK.
www.books-shop.com
7. 8. 9. 10. 11. 12. 13. 14.
15.
16.
В диалоговом окне Create New Class введите имя класса (например, CMyPage2) и убедитесь в том, что в качестве базового указан класс COlePropertyPage. Нажмите кнопку Create, а затем — кнопку OK в окне MFC ClassWizard. Найдите в главном файле реализации элемента (для элемента First — в файле FIRSTCTL.CPP) макрос BEGIN_PROPPAGEIDS. Увеличьте значение второго параметра макроса. Например, если теперь ваш элемент работает с двумя страницами свойств, 1 следует заменить на 2. Добавьте описание новой страницы под существующим описанием (или описаниями), но перед макросом END_PROPPAGEIDS. Новая строка должна выглядеть примерно так: PROPPAGEID(CMyPage2::guid). При помощи ClassWizard добавьте к странице новые элементы и функции DDX/DDV. При необходимости напишите код для обработки нестандартных ситуаций. Включите новый заголовочный файл в файл реализации элемента директивой #include. Создайте новый строковый ресурс для названия страницы (например, Page Two или чтонибудь столь же оригинальное) и идентификатор для него (например, IDS_PROPPGE2_FIRST_CAPTION). Перейдите к конструктору страницы и измените его, чтобы он передавал конструктору базового класса идентификатор названия — для этого необходимо заменить параметр 0 в вызове COlePropertyPage значением идентификатора, присвоенного данной строке. ControlWizard помещает над вызовом конструктора комментарий TODO. Для полноты картины после добавления строки его следует удалить! Создайте для новой страницы имя, которое должно быть занесено в реестр. Например, ControlWizard присваивает странице свойств по умолчанию имя First Property Page. По аналогии можно присвоить новой странице имя First’s Second Property Page. Добавьте в файл ресурсов новую строку с выбранным текстом и идентификатором наподобие IDS_PROPPGE2_FIRST (на самом деле имя идентификатора абсолютно несущественно, однако в MFC желательно соблюдать конвенцию). Проследите за тем, чтобы идентификатор строки передавался при вызове AfxOleRegisterPropertyPageClass, замените выбранным идентификатором последний параметр (0) в вызове этой функции, расположенном внутри функции UpdateRegistry фабрики класса страницы свойств. Данная функция фабрики класса должна находиться неподалеку от начала файла с исходным текстом новой страницы, а ее имя должно выглядеть примерно так: CMyPage2::CMyPage2Factory::UpdateRegistry. ControlWizard снова вставляет в этом месте комментарий, который следует удалить после выполнения указанного действия. Постройте проект заново.
Готово! У вас появилась новая страница свойств. Не знаю, ограничивается ли количество создаваемых страниц свойств, однако не следует забывать о том, что избыток страниц, прежде всего, отражается на многострадальном пользователе — добавляйте новые страницы лишь тогда, когда это действительно необходимо.
11.6 Стандартные страницы свойств В настоящее время на системном уровне реализованы три «стандартные страницы свойств», то есть заранее созданные страницы, которые вы можете добавить в свой элемент. Они предназначены для цветовых, шрифтовых и графических свойств. Эти страницы обладают своего рода «интеллектом» — они самостоятельно ищут элементы свойств, относящиеся к данному типу. Например, цветовая страница свойств, добавленная к элементу First, определит, что свойства BackColor и ForeColor относятся к типу OLE_COLOR, и позволит просмотреть, а также задать их значения. Чтобы добавить стандартную страницу свойств, найдите в исходном тексте главного модуля элемента фрагмент, в котором объявляются страницы свойств. В нашем примере этот фрагмент находится в файле FIRSTCTL.CPP и выглядит следующим образом:
////////////////////////////////////////////////////////////// // Страницы свойств // При необходимости добавьте дополнительные страницы свойств. // Не забудьте увеличить значение счетчика! BEGIN_PROPPAGEIDS(CFirstCtrl, 1) PROPPAGEID(CFirstPropPage::guid) END_PROPPAGEIDS(CFirstCtrl)
www.books-shop.com
Стандартные страницы добавляются следующим образом: 1. 2.
Увеличьте счетчик страниц в макросе BEGIN_PROPPAGEIDS. Добавьте описание соответствующей стандартной страницы. Например, для стандартной цветовой страницы необходимо вставить следующую строку:
PROPPAGEID(CLSID_CColorPropPage) сразу же после описания PROPPAGEID для первой страницы (для шрифтовых страниц используется CLSID_CFontPropPage, а для графических — CLSID_CPicturePropPage).
3.
Постройте элемент заново.
Одно из ограничений текущей реализации стандартных страниц свойств заключается в том, что их поведение является строго фиксированным. Например, если вам захочется, чтобы в шрифтовой странице отображались только пропорциональные шрифты, вам придется создавать собственную страницу. Начиная с MFC 4.0, Microsoft включает в библиотеку исходный текст стандартных страниц свойств, поэтому изменить их поведение оказывается проще, чем в предыдущих версиях. Стандартные страницы свойств автоматически читают библиотеку типов элемента и берут на себя ответственность за работу со всеми свойствами подходящего типа. Так, цветовая страница, добавленная в элемент First, позволяет работать со свойствами ForeColor и BackColor. Все отобранные свойства заносятся в раскрывающийся список, из которого можно выбрать нужное свойство и изменить его значение. Стандартные страницы ищут свойства по типу — например, цветовая страница выбирает свойства типа OLE_COLOR.
11.7 Использование справки в страницах свойств Включить справочную информацию в страницы свойств относительно просто. Для этого достаточно вызвать функцию COlePropertyPage::SetHelpInfo в классе, производном от COlePropertyPage. Функция SetHelpInfo получает три параметра:
Текст, который выводится в строке состояния или в виде подсказки, если такая возможность поддерживается фреймом свойств, в котором отображается данная страница. Имя справочного файла страницы свойств. Контекстный идентификатор для раздела справочного файла, относящегося к данной странице свойств. Вызов этой функции (при условии существования справочного файла и указанного раздела!) делает доступной кнопку Help на странице свойств. При ее нажатии вызывается функция WinHelp для заданного файла и раздела.
Как и в любом диалоговом окне, желательно обеспечить контекстную справку для каждого отдельного элемента. Вам придется самостоятельно написать соответствующий обработчик — интерпретировать страницу свойств как стандартное диалоговое окно, перехватывать клавишу F1 и вызывать функцию WinHelp с соответствующими параметрами.
11.8 Страницы свойств без MFC Добавить страницу свойств в элемент, не использующий MFC, не так уж сложно, однако при этом вы не получите помощи, которую предоставляет библиотека. Точнее, вы лишаетесь функций, которые автоматически пересылают информацию между элементами страницы и переменными вашей программы. Кроме того, затрудняется сам процесс установления соответствия между переменными и свойствами элемента.
www.books-shop.com
11.9 Интерфейсы, раскрываемые объектами страниц свойств Если вам захочется создать свою страницу свойств, необходимо раскрыть лишь два интерфейса — IPropertyPage и IPropertyPage2 (второй является производным от первого). При помощи этих интерфейсов фрейм, которому принадлежит данная страница (обычно узел страницы свойств), управляет ее работой. Обычно фрейм предоставляется системой в результате вызова функций OleCreatePropertyFrame и OleCreatePropertyFrameIndirect. В приведенной ниже таблице перечислены методы интерфейса IPropertyPage с краткими описаниями. Метод
Описание
SetPageSite
Предоставляет странице свойств указатель на ее узел (фрейм), точнее – указатель на интерфейс IPropertyPageSite узла.
Activate
Приказывает странице свойств отобразить себя в окне.
Deactivate
Приказывает странице свойств уничтожить окно, созданное функцией Activate.
GetPageInfo
Возвращает информацию о странице — название, размер и данные справочного файла.
SetObjects
Предоставляет странице доступ к объектам, для которых вызывается данная функция. Методу передается массив указателей на IUnknown, по одному для каждого объекта. Следовательно, если функция вызывается всего для одного элемента, то массив будет содержать один указатель, если она вызывается для n элементов, то и массив будет состоять из n указателей. Страница должна через полученные указатели обратиться с запросом QueryInterface для того интерфейса, посредством которого она собирается взаимодействовать с элементом (чаще всего это интерфейс Automation). Затем, когда возникнет необходимость записать свойство в объект (-ы), страница вызывает нужный метод через указатель (-и).
Show
Отображает или скрывает страницу.
Move
Перемещает или масштабирует страницу внутри фрейма.
IsPageDirty
Спрашивает страницу, изменилось ли ее содержимое (то есть является ли страница «грязной»). Данный флаг сбрасывается при вызове Apply.
Apply
Сообщает странице о том, что все текущие изменения должны быть применены к объекту (-ам), для которых она была вызвана.
Help
Вызывается, когда пользователь требует справку по данной странице.
TranslateAccelerator
Позволяет страницам свойств реагировать на нажатия клавишакселераторов.
Интерфейс IPropertyPage2 содержит один дополнительный интересный метод, который приказывает странице передать фокус конкретному элементу, определяемому значением dispid. Вы не обязаны поддерживать этот интерфейс на своих страницах, если только в вашем элементе не предусмотрена«работа на уровне отдельных свойств». Что этот означает? Существует специальный интерфейс IPerPropertyBrowsing. Если ваш элемент поддерживает его, он тем самым сообщает контейнеру, что в дополнение к IPropertyPage страницы свойств поддерживают и IPerPropertyPage2. Я кратко расскажу о работе на уровне свойств. Интерфейс IPerPropertyBrowsing содержит четыре метода (помимо методов IUnknown): GetDisplayString, MapPropertyToPage, GetPredefinedStrings и GetPredefinedValue. Первый метод возвращает описание свойства, которое может использоваться вместо имени, хранящегося в библиотеке типов. Второй метод, MapPropertyToPage, тесно связан с интерфейсом IPropertyPage2. Он возвращает CLSID объекта-страницы, посредством которого можно работать с заданным свойством. После того как страница заданного свойства будет найдена и отображена на экране (например, функцией OleCreatePropertyFrameIndirect, за которой следует вызов метода страницы IPropertyPage::Activate), можно перевести фокус и выделить для редактирования конкретное свойство методом IPropertyPage2::EditProperty.
Два оставшихся метода, GetPredefinedStrings и GetPredefinedValue, используются в том случае, когда свойство может принимать набор значений, не выражаемых в виде перечисляемого типа (как это было сделано для свойства TestProp в предыдущей главе). В этом случае GetPredefinedStrings возвращает массив с текстовыми описаниями возможных значений свойства, а также связанный с ним массив манипуляторов («волшебных чисел»), соответствующих этим строкам. Когда свойству присваивается одна из этих строк, его фактическое значение определяется при помощи метода GetPredefinedValue. Например, этот способ позволяет присвоить свойству значения, которые представляют собой степени 2 или что-нибудь в этом роде — удобство отображения для пользователя сочетается с несложной реализацией для разработчика.
www.books-shop.com
Глава
12
Классы ColeControl и ColePropertyPage В этой главе мы более подробно рассмотрим два основных класса библиотеки MFC, обеспечивающих работу элементов, — ColeControl и ColePropertyPage. До настоящего момента мы пользовались этими классами и их функциями, не особенно задумываясь над тем, как они устроены. Настало время познакомиться с этими классами поближе, узнать, какие функции в них имеются и когда их следует вызывать. Я не собираюсь превращать эту главу в справочное руководство или некий аналог документации MFC, к которой вы можете обратиться, если вам понадобится справочная информация.
ПРЕДУПРЕЖДЕНИЕ Во многих местах этой главы обсуждается специфика реализации и даже приводится исходный текст рассматриваемых классов. Учтите, что все сказанное справедливо лишь для MFC версии 4.2, существовавшей на момент написания книги (поставляется вместе с Microsoft Visual C++ версии 4.2). Вполне возможно, что механизм работы отдельных функций может измениться в последующих версиях MFC. Тем не менее я все же привожу исходные тексты, поскольку они наглядно демонстрируют многие нетривиальные аспекты работы этих классов. Главное — не стоит полагаться на эти технические детали!
ЗАМЕЧАНИЕ Данная глава представляет интерес лишь для тех разработчиков, которые собираются писать элементы на C++, пользуясь библиотекой MFC. Если вы пользуетесь ATL или любым другим инструментом, ничто из сказанного к вам не относится. Почему я подробно описываю классы MFC, а не классы ATL? Предполагается, что этой книгой будут пользоваться в основном разработчики, не обладающие опытом разработки элементов ActiveX. Опыт подсказывает, что в таких случаях MFC используется значительно чаще ATL. Если вам придется серьезно заниматься разработкой специализированных элементов для Internet, вероятно, вам стоит переключиться на ATL. Однако все, что вы узнаете о разработке элементов на базе MFC (конечно же, из этой книги!), принесет несомненную пользу.
12.1 ColeControl Как мы уже знаем, класс ColeControl является базовым для всех классов элементов ActiveX, создаваемых в MFC. Сам по себе это класс является производным от CWnd — обобщенного класса окна MFC. Класс CWnd содержит великое множество функций, многие из которых используются элементами ActiveX. Класс COleControl добавляет к ним целый набор новых функций, некоторых из них заменяют функции-прототипы CWnd. Почему? Потому что поведение элемента ActiveX в некоторых отношениях отличается от поведения обычного окна. Конечно, среди этих методов встречаются и другие, используемые только элементами ActiveX, — например, методы для инициирования событий, чтения свойств окружения, взаимодействия с контейнером или клиентским узлом.
12.2 Automation — свойства, методы и события Одна из групп функций класса ColeControl позволяет узнать значение любого свойства окружения. Эти функции называются Ambientxxx, где xxx — имя запрашиваемого свойства окружения. Они запрашивают значение свойства у узла элемента и возвращают его значение в
www.books-shop.com
том случае, если данное свойство поддерживается. Если контейнер не поддерживает данное свойство (или вообще никакие свойства окружения), функция возвращает значение по умолчанию. Кроме того, существует общая функция GetAmbientProperty, которой можно пользоваться для получения значений любых свойств окружения, включая нестандартные (то есть специфические для конкретного контейнера). Эта функция отличается от остальных тем, что она не пытается предоставить значение по умолчанию, поскольку не может заранее узнать, какое свойство окружения вас интересует. Если контейнер изменяет значение одного или нескольких свойств окружения узла элемента, он обращается к элементу, вызывая метод IOleControl::OnAmbientPropertyChange. При этом он передает dispid изменившегося свойства окружения или DISPID_UNKNOWN (–1), если одновременно изменилось сразу несколько свойств. COleControl перенаправляет вызов этой функции автору элемента через виртуальную функцию OnAmbientPropertyChange. Если вы захотите обнаруживать изменения свойств окружения и, вероятно, изменять внешний вид элемента, необходимо переопределить эту функцию в классе, производном от COleControl. Другая ситуация, также относящаяся к свойствам окружения, возникает при создании элемента. Если элемент установил бит OLEMISC_SETCLIENTSITEFIRST, он тем самым требует от контейнера создать клиентский узел для элемента перед тем, как загружать какие-либо параметры его устойчивого состояния. Следовательно, контейнер в этом случае должен предоставить элементу свойства окружения узла на момент загрузки. Чтобы определить, может ли элемент положиться на значения свойств окружения во время загрузки, вызовите функцию COleControl::WillAmbientsBeValidDuringLoad. Данная функция возвращает TRUE, если контейнер создает клиентский узел на достаточно ранней стадии и следовательно, во время загрузки элемент может пользоваться свойствами окружения, и FALSE — в противном случае. Если функция возвращает FALSE, следует прочитать устойчивое состояние элемента, а позже вернуться и изменить значения некоторых свойств в зависимости от значений свойств окружения узла. Следующая группа функций предназначена для инициирования событий. Для каждого стандартного события имеется соответствующая функция; например, событию Click соответствует функция FireClick. Вызов этой функции заставляет элемент инициировать событие Click. В текущей реализации все функции инициирования событий в конечном счете вызывают функцию инициирования «обобщенного события» FireEvent и чем-то напоминает функцию GetAmbientProperty. В число ее параметров входит dispid инициируемого события, а также произвольное количество параметров, определяемых типом события. В конце концов FireEvent вызывает реализацию IDispatch::Invoke, назначенную контейнером в качестве приемника события. Вообще говоря, вам не следует непосредственно вызывать FireEvent в своих программах. Вместо этого нужно средствами IDE сгенерировать данный тип события, вместе с которым будет сгенерирована и новая функция, вызывающая FireEvent. Рекомендуется именно так создавать и инициировать события, потому что созданные IDE функции обладают надежностью типов, а FireEvent — нет. Функция FireEvent пользуется классом COleDispatchDriver библиотеки MFC для того, чтобы выступить в роли контроллера Automation. Она получает интерфейс диспетчеризации для каждого подключения к точке соединения для событий (источнику) элемента и вызывает его метод Invoke. Код инициирования события приведен в листинге 12-1. Листинг 12-1. Фрагмент кода инициирования события из класса ColeControl (из MFC версии 4.2)
driver.InvokeHelperV(dispid, DISPATCH_METHOD, VT_EMPTY, NULL, pbParams, argList); END_TRY driver.DetachDispatch(); } } Функция FireEventV получает dispid события и список его параметров, преобразованных в стандартный тип va_list самой функцией FireEvent, вызывающей FireEventV (более подробная информация о списках переменных и типе va_list приведена в Microsoft Visual C++ Run-Time Library Reference). FireEventV создает экземпляр класса MFC COleDispatchDriver. Затем функция узнает у класса-оболочки для точки соединения, откуда начинается хранящаяся в нем коллекция подключений. FireEventV перебирает содержимое коллекции, получая указатель на IDispatch для каждого подключения. Надеюсь, вы не забыли, что точка соединения хранит указатель на интерфейс Automation для каждого подключения. Поскольку точка подключения для событий способна работать лишь с интерфейсами, производными от IDispatch, указатели на них можно безопасно преобразовать в указатели на IDispatch. Предполагается, что приемники событий не реализуются через двойственные интерфейсы. Так как в настоящее время не существует контейнеров, способных принимать события, направленные через двойственный интерфейс, пока такое предположение остается истинным, но в будущем все может измениться. Затем функция последовательно прикрепляет каждый указатель на интерфейс диспетчеризации к объекту COleDispatchDriver функцией AttachDispatch и через этот указатель вызывает Invoke (пользуясь вспомогательной функцией InvokeHelperV). Функция InvokeHelperV просто упаковывает все параметры в VARIANT и вызывает IDispatch::Invoke. Если при вызове инициируется исключение, оно перехватывается, но игнорируется. Затем функция отсоединяет интерфейс диспетчеризации текущего подключения и переходит к следующему подключению, входящему в коллекцию. Класс COleCOntrol содержит целую серию функций для работы со стандартными свойствами и методами. Например, функция GetText получает, а SetText задает текущее значение стандартного свойства Text или Caption (при установке одного из них автоматически устанавливается и второе). Функция InternalGetText вызывается при каждом получении значения свойства Text или Caption кодом элемента. Она отличается от функции GetText тем, что последняя вызывает InternalGetText, затем копирует возвращенную строку в BSTR и возвращает значение этого типа. Все стандартные свойства также помечены как связанные и обладают атрибутом RequestEdit. Это означает, что при попытке изменить значение стандартного свойства элемент сначала спрашивает разрешения у контейнера, вызывая функцию COleControl::BoundPropertyRequestEdit. Если контейнер отвечает положительно (или игнорирует запрос — это происходит, если контейнер ничего не знает о связывании данных),— свойство изменяется, а элемент сообщает об этом контейнеру функцией COleControl::BoundPropertyChanged.
ЗАМЕЧАНИЕ Связывание данных более подробно рассматривается в главе 15, но я надеюсь, что вы уже поняли — от идеи «связать свойство с полем базы данных» мы перешли к идее «предоставить контейнеру право наблюдать за изменением свойства».
MFC сообщает элементу об изменении стандартного свойства, вызывая соответствующую функцию элемента. Если не переопределить ее, будет использована реализация базового класса. Обычно она вызывает функцию InvalidateControl, чтобы заставить элемент перерисовать себя, и что-нибудь делает с новым значением свойства. Примером может послужить функция OnTextChanged, которая вызывается при изменении свойства Text или Caption. Если не переопределить OnTextChanged в производном классе, вызывается версия этой функции класса COleControl, которая заставляет элемент перерисовать себя. В большинстве случаев такое поведение по умолчанию оказывается вполне достаточным. Тем не менее иногда при изменении стандартного свойства необходимо сделать что-то особенное. Класс ColeControl предоставляет вам такую возможность.
www.books-shop.com
12.3 Безопасные преобразования типов Одно из преимуществ таких языков, как C++, состоит в том, что они позволяют писать «безопасные» функции доступа. Что это значит? Представьте себе, что у вас имеется функция, которая может получать несколько параметров различных типов (например, стандартная библиотечная функция printf). Компилятор никак не может проверить правильность передаваемых printf параметров, поскольку эта функция изначально определена как получающая список параметров переменного типа. Компилятор может определить лишь правильность типа первого параметра (строки формата), поскольку этот параметр известен заранее. Следовательно, функцию printf нельзя считать «безопасной». Безопасная версия printf фактически представляет собой целое семейство функций, каждая из которых определена для определенного набора параметров. Например, версию printf, предназначенную только для вывода целых чисел, можно было бы определить следующим образом:
int IntPrintf (const char *pszFormat, int nNumber) { return printf(pszFormat, int nNumber) } Для подобных безопасных функций даже не потребуется C++. Универсальная функция инициирования события COleControl::FireEvent, описанная в этой главе, не является безопасной, однако ее можно «завернуть» в специальные функции-оболочки. Преимущества? При обработке исходного текста компилятор сможет убедиться в том, что вы правильно вызываете функцию и передаете ей параметры правильного типа (или такие, которые могут быть в него преобразованы). Для обеспечения безопасности типов в C++ также применяются шаблоны. Шаблоны имеют нечто общее с макросами и представляют собой механизмы для определения параметризованных наборов классов или функций, которые могут применяться для различных типов данных. При использовании шаблона программист указывает тип, который должен быть подставлен вместо обобщенного типа-параметра. Компилятор создает новый класс на основании шаблона и заданного типа. Примерами шаблонов в библиотеке MFC являются классы CArray и CList. Библиотека ATL (ActiveX Template Library) целиком построена на использовании шаблонов. В некоторых ситуациях (и обычно во время инициализации элемента) контейнер не может обрабатывать события. Это не означает, что попытка инициировать событие закончится аварийно (такое может случиться лишь для очень плохо написанного контейнера) — просто событие не будет обработано. Контейнер сообщает элементу о том, что он игнорирует события,вызывая метод IOleControl::FreezeEvents с параметром TRUE. Вызов этой же функции с параметром FALSE сообщает элементу о том, что события снова обрабатываются. Вызов метода перенаправляется программисту элемента через виртуальную функцию COleControl::OnFreezeEvents. Переопределяя ее, вы можете игнорировать события во время блокировки, сохранять их на будущее или, например, сохранять лишь самые важные события, которые позднее нужно будет инициировать заново. К этому же разряду относится и функция COleControl::OnEventAdvise, которая вызывается при успешном подключении к точке соединения для событий элемента. Например, если к точке соединения еще никто не подключился, инициировать событие бессмысленно. С другой стороны, у вас может появиться исключительно важное событие, которое должно быть гарантированно получено всеми подключениями. Для этого можно переопределить функцию OnEventAdvise и позаботиться о том, чтобы событие инициировалось при каждом вызове функции (разумеется, если события в этот момент не заблокированы). Возможно, вы еще помните, что функции OnFreezeEvents и OnEventAdvise использовались в ранней версии элемента First для того, чтобы сообщать клиентам о неудачных попытках поиска или открытия файлов (индексного и сообщений). Если вы работаете с клавиатурой при помощи стандартных событий (например, KeyDown), иногда бывает полезно переопределить функцию OnKeyDownEvent, вызываемую библиотекой после инициирования события. Данная функция (и/или другие функции того же семейства, если вы обрабатываете другие стандартные события от клавиатуры) используется в тех случаях, когда внутреннюю обработку нажатых клавиш приходится совмещать с инициированием события.
www.books-shop.com
12.4 Обработка ошибок и исключения Automation В главе 9 говорилось об ошибках и обработке исключений. Элементы ActiveX могут инициировать как события-ошибки за пределами вызовов Automation, так и стандартные исключения Automation, происходящие во время вызова методов и обращений к свойствам. Класс COleControl содержит несколько функций, упрощающих вашу задачу. Чтобы создать свойство, доступное только для чтения, обычно следует удалить в IDE имя функции записи свойства. В этом случае IDE заменяет функцию записи в макросе, создающем свойство, вызовом функции COleControl::SetNotSupported. Данная функция просто инициирует исключение Automation функцией ThrowError. Аналогичная функция существует и для свойств, доступных только для записи; она называется GetNotSupported. Еще одна похожая функция вызывается, когда элемент спрашивает у контейнера разрешение на изменение связанного свойства (функцией BoundPropertyRequestEdit). Если контейнер отвечает положительно, элемент продолжает делать то, что считает нужным, в противном случае элемент должен инициировать исключение, для чего он вызывает функцию SetNotPermitted. Нетрудно представить себе и другие ситуации, в которых элемент может захотеть воспользоваться тем же механизмом — то есть запретить изменение некоторого свойства в определенный момент времени, поскольку не соблюдено некоторое условие. Тем не менее в таких случаях не следует прибегать к функции SetNotPermitted для того, чтобы сообщить пользователю об ошибке, поскольку эта функция инициирует код ошибки, предназначенный лишь для отказа в редактировании связанных свойств. Если тот же самый код будет использоваться для отчасти похожей, но на самом деле принципиально иной ситуации, он лишь станет причиной недоразумений. Следовательно, для подобных ситуаций вам придется создать свой собственный HRESULT. Функция ThrowError подробно рассматривается в главе 9. Она существует в двух версиях: первая получает в качестве параметра-описания строку, а другая — идентификатор ресурса. Единственное отличие между этими двумя версиями заключается в том, что последняя загружает строку из файла ресурсов. В большинстве случаев используется именно вторая версия функции ThrowError. Первая версия обычно встречается тогда, когда описание ошибки должно быть построено во время выполнения программы (например, для ошибок ODBC). Тем не менее преимущества от хранения статического текста в файле ресурсов оказываются достаточно наглядными, чтобы во всех случаях, когда это возможно, использовался именно такой подход. Каковы же эти преимущества? Во-первых, строки будут загружаться в память только в случае необходимости, во-вторых, это существенно облегчает процесс локализации программы для другого языка. При ближайшем рассмотрении функции ThrowError выясняется, что она выглядит достаточно прямолинейно. В основном ее работа сводится к инициированию исключения COleDispatchExceptionEx. Этот класс исключений является прямым потомком стандартного класса исключений MFC COleDispatchException и отличается от него лишь тем, что вместо переменной m_wCode заполняется поле HRESULT исключения Automation. Согласно спецификации Automation, эти два поля являются взаимно исключающими: если одно из них заполняется, то другое должно оставаться пустым. Когда элемент ActiveX, инициируя событие Error, сообщает об ошибке контейнеру, последний имеет возможность ее обработать. Кроме того, последний параметр самого события представляет собой указатель на логическую переменную, которой элемент перед инициированием события присваивает значение FALSE. Если контейнер не изменит значения этой переменной, то после возвращения из принадлежащего контейнеру обработчика ошибки элемент вызывает функцию COleControl::DisplayError (или переопределяющую функцию, если вы ее создадите). Принятая по умолчанию реализация этой функции выводит окно сообщения, содержащее строку с описанием ошибки. Если вам захочется получить более подробную информацию об ошибках или вести протокол ошибок, достаточно переопределить эту функцию в своем производном классе.
12.5 Функции, обеспечивающие устойчивость свойств Класс COleControl содержит несколько функций, облегчающих сохранение и загрузку свойств элемента в устойчивом хранилище. Некоторые из этих функций чаще всего используются незаметно для программиста — например, функции ExchangeExtent и ExchangeStockProps вызываются стандартной версией функции DoPropExchange, создаваемой OLE ControlWizard. Функция DoPropExchange читает и записывает свойства в хранилище (или туда, куда прикажет контейнер), вызывает ExchangeExtent для сохранения или восстановления текущего размера элемента и ExchangeStockProps — для сохранения и восстановления любых стандартных свойств,
www.books-shop.com
используемых элементом. Последняя функция при помощи «маски» (набора битов) определяет, какие стандартные свойства используются элементом. Маска также сохраняется и восстанавливается из устойчивого хранилища.
ДЛЯ ВАШЕГО СВЕДЕНИЯ Если ваш элемент использует стандартные свойства, но тем не менее вы не желаете сохранять какие-либо (или все) из них, лучше всего написать собственную версию ExchangeStockProps. Это гораздо проще, чем мучиться с маской стандартных свойств, формат которой к тому же зависит от реализации и может измениться в будущем.
Другая функция, которая также обычно вызывается автоматически, — ExchangeVersion. Она сохраняет и восстанавливает номер версии свойств, обычно совпадающий с номером версии элемента. Тем не менее если новый элемент читает данные, относящиеся к старой версии элемента, он может по полученному от ExchangeVersion номеру версии определить, для каких новых свойств нужно создать значения по умолчанию и какие старые, неиспользуемые ныне свойства следует проигнорировать. При изменении свойства (либо самим элементом в результате каких-либо обстоятельств, либо на программном уровне, либо в результате взаимодействия с пользователем через страницы свойств) элемент должен изменить свое внутреннее состояние, вызвав функцию SetModifiedFlag. Это приводит к тому, что функция IsModified начнет возвращать значение TRUE — тем самым она показывает, что элемент «загрязнился» и по требованию контейнера ему следует предоставить возможность сохранить обновленное состояние. К устойчивости свойств относятся и две другие функции класса COleControl. Первую из них, WillAmbientsBeValidDuringLoad, элемент может вызывать во время инициализации, чтобы определить, сможет ли он получать от контейнера значения свойств окружения в процессе загрузки. Установленный для элемента флаг OLEMISC_SETCLIENTSITEFIRST свидетельствует о том, что элемент требует предоставить ему такую возможность. На это контейнер должен среагировать, вызывая IOleObject::SetClientSite перед тем, как приказывать элементу загрузить устойчивые свойства. При создании клиентского узла контейнером вызывается функция OnSetClientSite. Вы должны переопределить ее, если хотите производить какие-нибудь нестандартные действия на этом этапе. Вторая функция, которая относится к устойчивости свойств — IsConvertingVBX, — со временем будет встречаться все реже и реже. Данная функция возвращает TRUE, если элемент определит, что загружаемые им свойства были сохранены старой VBX-версией элемента. Напомню, что VBX, или нестандартные управляющие элементы Visual Basic, отчасти напоминают элементы ActiveX, однако обладают более ограниченными возможностями. На панели элементов Microsoft Visual Basic 4.0 содержатся элементы ActiveX вместо VBX, входивших в более старые версии Visual Basic. Среди возможностей, предоставляемых этой версией Visual Basic, — автоматическое преобразование любых VBX, используемых в программе, в эквивалентные им элементы ActiveX. Для этого ссылки на VBX заменяются ссылками на эквивалентные элементы ActiveX, а сохраненные свойства VBX преобразуются в свойства, совместимые с элементами ActiveX. Существенное отличие элементов ActiveX от VBX заключается в свойстве Font, которое теперь представляет собой вложенный элемент ActiveX с собственным набором атрибутов. Эти атрибуты сохраняются в файле экранной формы Visual Basic (.FRM) в виде отдельного блока. В VBX шрифты сохраняются в виде отдельных свойств — например, FontSize, FontName и FontItalic. В некоторых ситуациях обобщенный механизм устойчивости, предоставляемый функцией DoPropExchange, оказывается неэффективным. При желании вы можете организовать более эффективную сериализацию свойств элемента, переопределяя функцию COleControl::Serialize в классе вашего элемента. По умолчанию функция Serialize вызывает DoPropExchange. Если вы решитесь изменить поведение Serialize, вам могут пригодиться дополнительные функции SerializeExtent, SerializeStockProps и SerializeVersion. Каждая из них получает в качестве параметра ссылку на CArchive и позволяет точно определить, как должны происходить чтение и запись двоичного состояния элемента в хранилище.
12.6 Функции, относящиеся к ActiveX
www.books-shop.com
Помимо многочисленных функций, обеспечивающих поддержку Automation для работы со свойствами, событиями, методами и свойствами окружения, класс COleControl также содержит разнообразные функции, связанные с различными аспектами ActiveX. Например, функция SetControlSize задает размеры элемента и затем вызывает метод IOleObject::SetExtent, чтобы задать физические размеры элемента в контейнере. Связанная с ней функция GetControlSize оптимизирована — ей не приходится обращаться к интерфейсу ActiveX, поскольку SetControlSize осуществляет внутреннее кэширование данных о размерах. Функция SetRectInContainer работает аналогично, за исключением того, что она также позволяет перемещать элемент внутри контейнера. Парная ей функция GetRectInContainer получает размеры элемента и его позицию по отношению к контейнеру. Элемент должен вызвать функцию ControlInfoChanged при изменении состава обрабатываемых им мнемонических сокращений (мнемоник). Функция обращается к контейнеру и приказывает ему вызвать IOleControl::GetControlInfo для получения обновленной информации. Это, в свою очередь, приводит к вызову функции OnGetControlInfo класса COleControl. Если контейнер поддерживает концепцию расширенных элементов, элемент может вызвать функцию GetExtendedControl, чтобы получить указатель на интерфейс Automation для расширенного элемента. Категорически не рекомендуется полагаться на факт наличия расширенных элементов, поскольку они поддерживаются не всеми контейнерами — это требование вовсе не является обязательным. Иногда бывает необходимо гарантировать, что в определенных обстоятельствах (например, при инициировании события) ваш элемент не будет деактивизирован. В таких случаях следует вызвать функцию LockInPlaceActive с параметром TRUE, который сообщает контейнеру, что сейчас деактивизировать элемент было бы крайне нежелательно. В дальнейшем этот вызов должен быть уравновешен парным вызовом LockInPlaceActive с параметром FALSE, причем элемент не должен злоупотреблять этой возможностью и ограничивать свободу действий контейнера на длительный период. К этой же теме относятся и функции PreModalDialog и PostModalDialog. Элемент должен «вкладывать» любые модальные вызовы диалоговых окон между этими двумя функциями, чтобы сообщить контейнеру о том, что он переходит в модальное состояние и просит контейнер сделать то же самое. Если пропустить эти функции, контейнер будет продолжать работать так, словно никакого модального окна нет. В лучшем случае это будет выглядеть нелепо, в худшем — приведет к непредсказуемым проблемам (скажем, если пользователь контейнера попытается удалить элемент!). Когда контейнер желает UI-активизировать элемент, он вызывает либо первичную команду элемента (OLEIVERB_PRIMARY), либо команду OLEIVERB_UIACTIVATE. Впрочем, элемент может самостоятельно UI-активизироваться, для чего вызывает свою функцию OnEdit. Если вам потребуется написать элемент, работающий с нестандартными командами OLE, можно добавить новые записи в схему сообщений, используя макрос ON_OLEVERB. Такие записи автоматически нумеруются элементом, когда контейнер обращается к нему с требованием перенумеровать команды. Вы можете переопределить стандартную схему нумерации команд, переопределив функцию COleControl::OnEnumVerbs (впрочем, не могу себе представить, для чего это может понадобиться). Когда элемент должен перерисовать себя, вызывается его функция OnDraw. Если контейнер не активизирует элемент (некоторые контейнеры поступают таким образом — например, Microsoft Access 2.0 в режиме конструирования), он может потребовать, чтобы последний нарисовал себя в метафайле Windows. Это приводит к вызову функции OnDrawMetafile. По умолчанию функция OnDrawMetafile просто вызывает OnDraw. Функция OnDraw, создаваемая мастером OLE ControlWizard, по умолчанию рисует эллипс! Разумеется, вы должны изменить ее поведение, чтобы данная функция правильно рисовала содержимое вашего элемента. Если ваша реализация OnDraw выполняет какие-то действия, не разрешенные для метафайлов, необходимо проследить, чтобы переопределенная версия OnDrawMetafile ограничивалась лишь разрешенными действиями. В частности, рисование в метафайле оказывается полезным при создании элемента, который представляет собой подкласс стандартного элемента Windows — например, поля со списком (combo box). К сожалению, некоторые из этих элементов содержат (по крайней мере, в Windows 3.x) ошибки, которые приводят к их неверному отображению в метафайлах. При таких обстоятельствах вам придется написать свой собственный код для рисования таких элементов. Если вам потребуется обновить внешний вид элемента в произвольный момент времени, вы можете имитировать посылку сообщения WM_PAINT самому себе, вызывая функцию InvalidateControl. Данная функция получает необязательный параметр, который указывает, какая часть прямоугольника элемента объявляется недействительной. Разумно написанная функция
www.books-shop.com
рисования может воспользоваться этим параметром для того, чтобы сделать обновление изображения более эффективным. Хотя внешне функция InvalidateControl напоминает стандартную функцию Windows API InvalidateRect (или ее аналог в классе CWnd), не пытайтесь прибегнуть к услугам Windows API вместо того, чтобы вызывать ее. Почему? Да потому что неактивный элемент не имеет окна, однако он может осуществлять вывод в предоставленный контейнером метафайл или (что встречается чаще) в окно контейнера. Контейнер обратится с требованием перерисовать метафайл лишь в том случае, если вы воспользуетесь функцией InvalidateControl, поскольку эта функция распознает такую ситуацию и обращается к контейнеру при помощи метода IAdviseSink::OnViewChange. Стандартная функция InvalidateRect этого не сделает. Иногда элемент должен вести себя так, словно он только что был инициализирован — например, при неудачной попытке загрузить его свойства. Для этого применяется функция OnResetState, а OLE ControlWizard обеспечивает ее переопределение в классе элемента. Впрочем, по умолчанию переопределенная функция всего лишь вызывает версию базового класса. В свою очередь, последняя функция вызывает DoPropExchange для чтения свойств. Все остальные инициализирующие действия, выполняемые во время сброса, должны происходить именно здесь. В справочной системе Visual C++ Books Online перечисляются точки, в которых данная функция может вызываться в библиотеке MFC.
12.7 OCX 96 и расширения ActiveX в классе COleControl В классе COleControl появилось немало новых функций, поддерживающих возможности OCX 96 и ActiveX. Большинство этих функций — виртуальные, которые можно переопределить, хотя некоторые из них являются чисто информационными и предназначаются лишь для определения текущего состояния элемента. Функции GetFocus и SetFocus используются для поддержки внеоконной работы элемента — с их помощью можно захватить фокус и определить факт его наличия во внеоконном режиме. Аналогично, при помощи функций GetCapture и SetCapture можно захватить курсор мыши и проверить, был ли он захвачен ранее. Функция ReleaseCapture возвращает захваченный курсор контейнеру. Функции GetDC и ReleaseDC позволяют получить и освободить DC для области рисования элемента, благодаря чему элемент может осуществлять графический вывод независимо от поступления сообщения. Достаточно полезные функции ClientToParent и ParentToClient преобразуют координаты точки из системы координат окна контейнера в координаты окна элемента, и наоборот. Функция GetClientOffset возвращает смещение клиентской области по отношению ко всему прямоугольнику элемента. Она оказывается чрезвычайно полезной в тех случаях, когда элемент содержит полосы прокрутки, границы и т. д. Функция ClipCaretRect определяет, какая часть каретки (текстового курсора) может отображаться элементом. Функция GetWindowlessDropTarget возвращает указатель на интерфейс приемника для внеоконного элемента при выполнении операции drag-and-drop. Функция OnWindowlessMessage вызывается при получении элементом сообщения Windows (кроме сообщений от мыши или клавиатуры). Обычно эта функция переопределяется в производном классе, чтобы обеспечить с его стороны нужную реакцию на сообщения Windows. Единственное принципиальное отличие данной функции от классической процедуры окна Windows заключается в том, что с сообщением не ассоциируется отдельное значение HWND, поскольку оно пользуется окном контейнера. Наконец, функция ScrollWindow позволяет целиком или частично прокрутить область внеоконного элемента. Для работы в неактивном состоянии в классе COleControl предусмотрена функция GetActivationPolicy, которая обеспечивает неактивное поведение элемента по умолчанию (если ее не переопределить). Кроме того, имеются функции OnInactiveMouseMove и OnInactiveSetCursor, которые вызываются при получении элементом соответствующих «сообщений» в неактивном состоянии. Функция IsOptimizedDraw определяет, поддерживает ли контейнер оптимизацию графического вывода из спецификации OCX 96. Если результат проверки оказывается положительным, элемент может выполнять некоторые нестандартные действия — например, оставлять в DC выбранные кисти и перья, вместо того чтобы восстанавливать старые объекты. Для асинхронной работы со свойствами COleControl содержит несколько функций, задача которых — обеспечить работу со свойством ReadyState и связанным с ним событием. Функция
www.books-shop.com
FireReadyStateChange инициирует событие ReadyStateChange, посылаемое контейнеру; предполагается, что это происходит при каждом изменении свойства ReadyState. Функция GetReadyState получает текущее значение данного свойства. Функция InternalSetReadyState присваивает свойству передаваемое значение и инициирует событие ReadyStateChange. Метод Load используется для того, чтобы сбросить асинхронное свойство и заново загрузить его по заданному URL. Осталось рассмотреть лишь функции, относящиеся непосредственно к интерфейсу IViewObject и его многочисленным методам. При каждом вызове метода интерфейса IViewObject вызывается соответствующая функция. Переопределяя ее, можно делать что-то нестандартное, а в тех случаях, когда по умолчанию не делается вообще ничего — делать хоть что-то! В число этих функций входят OnGetNaturalExtent, OnGetViewExtent, OnGetViewRect, OnGetViewStatus, OnQueryHitPoint и OnQueryHitRect. Функция OnGetViewExtent может использоваться для динамического изменения размеров области рисования и, следовательно, для поддержки рисования в два прохода. Обычно она используется в сочетании с функцией OnGetViewStatus. При помощи функции OnQueryHitPoint элемент определяет, где находится та или иная точка — в пределах элемента, вне его или в некоторой области, которую элемент считает расположенной «вблизи». Принадлежность точки определяется самим элементом, поскольку точка может находиться на прозрачной части элемента. В таких случаях элемент обычно считает, что точка находится снаружи. Наконец, функция OnQueryHitRect сообщает контейнеру, соприкасается ли передаваемый прямоугольник с элементом хотя бы в одной точке.
12.8 ColePropertyPage Класс-оболочка MFC для работы со страницами свойств, COlePropertyPage, является производным от CDialog — стандартного класса диалогового окна библиотеки MFC. В число его предков входит и класс CWnd, который является базовым для CDialog. Соответственно, класс COlePropertyPage содержит немало функций, общих с классом COleControl, а также множество новых, специализированный функций. Начиная с MFC 4.0, класс CWnd может включать элементы ActiveX, следовательно, страница свойств MFC также может включать элементы ActiveX, в том числе и тот, которому принадлежит данная страница (как бы странно это ни было)! Обобщенная страница свойств устроена гораздо проще и выполняет гораздо более конкретные задачи, нежели обобщенный элемент ActiveX, поэтому нет ничего удивительного в том, что класс COlePropertyPage выглядит намного проще класса COleControl. При создании нового элемента OLE ControlWizard включает в него пустое диалоговое окно для страницы свойств, а также класс, производный от COlePropertyPage. Объект класса COlePropertyPage имеет смысл лишь при наличии связанного с ним ресурса диалогового окна Windows. Класс COlePropertyPage, как и стандартный класс CDialog, содержит функцию OnInitDialog, которая вызывается непосредственно перед отображением диалогового окна, связанного с данной страницей. Если инициализация должна сопровождаться какими-либо нестандартными действиями, необходимо переопределить эту функцию и наделить ее новыми возможностями. Хотя идентификатор ресурса диалогового окна обычно передается объекту COlePropertyPage в конструкторе, вы можете создать объект страницы динамически и затем передать ему находящийся в памяти ресурс — для этого следует вызвать функцию SetDialogResource. Данная возможность оказывается полезной при динамическом создании страниц свойств во время выполнения программы. Другой параметр, передаваемый объекту COlePropertyPage во время выполнения конструктора, — идентификатор строки, определяющей название страницы. Название также может быть задано динамически функцией SetPageName, параметром которой является обычная строка, а не строковый идентификатор. Пользуясь функцией SetPageName, можно создавать строку названия во время выполнения программы — например, чтобы название отражало особенности текущего состояния элемента. Страницы свойств представляют собой типичный пользовательский интерфейс, и потому наличие справки в них считается почти обязательным требованием. Для облегчения этой задачи класс COlePropertyPage содержит функцию SetHelpInfo со следующими параметрами: текст, отображаемый в строке состояния или подсказке, имя справочного файла с информацией о данной странице, а также контекстный идентификатор для справочного файла. Наибольший интерес представляет первый параметр, поскольку он позволяет контейнеру, предоставляющему фрейм свойств (то есть диалоговое окно, в котором выводятся все страницы), получить текст от самой страницы и затем отобразить его где-либо при выделении определенного элемента страницы или перемещении над ней курсора. Другая функция, OnHelp, вызывается, когда
пользователь потребует справку по странице свойств (например, нажав кнопку Help). Обычно переопределять эту функцию незачем, но при желании это можно сделать, чтобы предоставить нестандартную справочную информацию. Контейнеры получают всю информацию о страницах свойств, в том числе и справочный текст (см. предыдущий абзац), при помощи интерфейса IPropertyPage или IPropertyPage2. Чтобы сообщить странице свойств информацию о себе, контейнер пользуется методом IPropertyPage::SetPageSite. Данная функция предоставляет странице интерфейс ActiveX, через который она сможет при необходимости взаимодействовать с контейнером. При вызове этого метода фреймом класс COlePropertyPage вызывает функцию OnSetPageSite. Если вдруг вам понадобится изменить поведение по умолчанию, воспользуйтесь этой функцией. После ее вызова страница свойств может вызвать функцию GetPageSite, которая возвращает указатель на интерфейс IPropertyPageSite ее узла. Интерфейс IPropertyPage2 идентичен интерфейсу IPropertyPage, за исключением того, что IPropertyPage2 содержит один дополнительный метод для передачи фокуса заданному элементу на странице. О вызовах этого дополнительного метода класс COlePropertyPage сообщает программисту через функцию OnEditProperty.
ЗАМЕЧАНИЕ Для соблюдения семантики интерфейса необходимо переопределить OnEditProperty так, чтобы фокус передавался тому свойству, dispid которого передается в качестве параметра. По умолчанию данная функция не делает ничего. Более подробная информация приведена в главе 11, «Страницы свойств».
В обычной ситуации страница свойств работает с одним экземпляром элемента. Тем не менее никто не запрещает вам выделить несколько элементов одинакового типа (например, на форме Visual Basic) и затем вызвать страницу свойств, которая бы обслуживала сразу несколько элементов. Изменение свойств будет происходить сразу во всех выделенных элементах. Страница свойств содержит список объектов, для которых она была вызвана. Список хранится в виде массива указателей на IDispatch. Для получения этих указателей можно воспользоваться функцией GetObjectArray класса COlePropertyPage. Не вызывайте Release для этих указателей, если только предварительно вы не вызвали метод AddRef! Когда пользователь изменяет значение свойства при помощи страницы свойств, последняя может отметить факт своего изменения, вызывая функцию SetModifiedFlag. Значение этого флага можно получить при помощи функции IsModified (как и для класса COleControl). Для того чтобы отслеживать вносимые пользователем изменения, для каждого элемента на странице свойств поддерживается флаг. Он определяет, изменялось ли соответствующее значение. Значение флага можно получить функцией GetControlStatus и задать функцией SetControlStatus. Когда пользователь закрывает страницу свойств или нажимает кнопку Apply, происходит обновление тех свойств, которые помечены как изменившиеся. Кнопка Apply обычно становится доступной после внесения изменений хотя бы в один элемент на странице. Если у вас имеются элементы, которые не должны влиять на доступность кнопки Apply, вызовите для каждого из них функцию IgnoreApply. Класс страницы свойств перестает следить за такими элементами и не разблокирует кнопку Apply при их изменении.
Глава
13 www.books-shop.com
Элементы ActiveX для профессионалов Элементы ActiveX и Internet Когда-то элементы OLE рассматривались как важнейшие компоненты таких программных сред, как Microsoft Visual Basic, Microsoft Visual C++, Microsoft Visual FoxPro, Microsoft Access. С тех пор мир изменился. Элементы OLE были переименованы в элементы ActiveX. Критерии того, что же можно считать элементом, стали заметно более либеральными, а элементы ActiveX стали играть ключевую роль в стратегии Microsoft по насыщению некогда статичных страниц World Wide Web живым, эффектным содержанием. Неожиданное (по крайней мере, для Microsoft!) пришествие Internet в 1995 году заставило нас полностью пересмотреть всю стратегию. Умные головы в Microsoft осознали, какое влияние элементы OLE могут оказать на развитие этого феномена, однако они поняли и другое — текущее состояние элементов сопряжено с многочисленными проблемами, которые не позволят превратить элементы в универсальную панацею для «активного содержания». Несомненно, OLE и соответствующая методика управления объектами формировали неплохую основу, однако в технологию пришлось внести некоторые изменения, благодаря которым элементы подошли бы типичным пользователям Web, подключенным к Internet через модем. Особенно важным оказывался объем пересылаемого кода: элементы в своем исходном варианте не отличались особой компактностью, а производительность их работы, мягко говоря, не слишком хорошо отвечала требованиям Web-страниц! На тот момент у нас была почти готова новая спецификация элементов — OCX 96. Она решила некоторые проблемы, однако на многие вопросы еще предстояло ответить в будущем. Например, должен ли элемент, предназначенный для работы в Web, поддерживать многочисленные интерфейсы, которые требовались от элементов OLE и о которых типичные пользователи Web-страниц даже понятия не имели? Или какие эмоции испытывает пользователь, когда на странице загружаются свойства большого объема — например, графика и другие мультимедиа-данные? Вряд ли пользователю понравится, что страницу нельзя будет прочитать до завершения пересылки этих данных. Предложенное Microsoft фундаментальное решение представляло собой семейство технологий, которое первоначально называлось «Sweeper», но потом вместе со всеми технологиями на базе COM (не считая традиционного связывания и внедрения) было переименовано в ActiveX. Пользуясь средствами ActiveX, можно создавать Web-броузеры с самыми разнообразными возможностями, среди которых — поддержка Internet-протоколов, анализ URL, кэширование информации, ее постепенное воспроизведение и пересылка и электронные подписи (сигнатуры). Первым серьезным приложением, в котором использовались все эти возможности, стал Microsoft Internet Explorer 3.0, версия которого для Microsoft Win32 на Intel-платформах появилась в августе 1996 года. Дальнейшим развитием той же технологии стал Microsoft Internet Explorer 4.0, в котором метафора броузера распространяется на традиционный рабочий стол Windows и в корне изменяет привычные способы взаимодействия с компьютером. Лично для меня самые большие изменения начались с проведенной Microsoft в марте 1996 года конференции профессиональных разработчиков для Internet после которой мне пришлось срочно приниматься за подготовку второго издания книги. Поскольку сейчас вы читаете плоды моих усилий, считайте, что революция Internet коснулась и вас. Никогда не перестаю поражаться тому, насколько расширились возможности Web. Впервые я начал развлекаться с Web в середине 1995 года, пользуясь уникальным, почти ни на что не годным Internet Explorer 1.0. Это занятие мне довольно быстро надоело. Хотя уже тогда в Web содержалось немало информации, она редко обновлялась и обычно была довольно бестолковой. Тем не менее с каждым месяцем в Web появлялось все больше и больше интересного, так что сейчас определенную информацию я предпочитаю черпать исключительно из Web. В частности, я перестал читать бумажные газеты и журналы компьютерной тематики и лишь изредка читаю ежедневные издания. Более того, теперь я могу вернуться к своим национальным корням и читать английские электронные газеты, содержащие новости с Родины. Наконец, во время Олимпийских игр 1996 года мне не понравилось, как телекомпания NBC освещала ход соревнований. Хотя моя жена считает, что по части развлечений компьютер стоит примерно на одном уровне с бензопилой, мне все же удалось убедить ее воспользоваться Web-сайтом BBC для получения более полной олимпийской информации. В частности, чудеса RealAudio позволили нам прослушать записи трансляций и узнать о том, как неудачно выступили английские атлеты.
www.books-shop.com
Web стала настолько вездесущей, что большинству программистов приходится считаться с ней. До сих пор не утихли дебаты о том, что лучше — апплеты Java или элементы ActiveX? Microsoft полагает, что их вообще не стоит разграничивать, то есть апплет Java может пользоваться элементами ActiveX или вообще представлять собой элемент. По этой причине Microsoft включила в свою виртуальную машину (VM) Java поддержку COM. Считается, что в настоящее время Java лучше обеспечивает совместимость на различных платформах: поскольку апплеты Java компилируются в интерпретируемый байт-код и могут пользоваться лишь определенным набором библиотек (впрочем, это не совсем так — Java обладает механизмом для вызова «родных» функций, написанных на C, а виртуальная машина Microsoft позволяет работать с произвольными COM-объектами, но общий смысл вы наверняка уловили), апплеты могут работать на любой платформе, поддерживающей правильную реализацию Java VM и сопутствующих библиотек. Элементы ActiveX должны быть скомпилированы для той платформы, на которой они будут работать, поэтому чаще всего приходится иметь отдельный двоичный вариант элемента для платформы Win32 на базе Intel, отдельный вариант для платформы Macintosh и т. д. Сервер должен предоставить клиенту правильную версию элемента при работе с Web-страницей, содержащей данный элемент. Как это делается? Большинство броузеров может по запросу Internet-сервера сообщать ему некоторые сведения о себе, в частности, тип броузера и платформу, на которой он работает. Нормальный сервер (к этой категории относятся все популярные Internet-серверы) может определить, какую версию элемента он должен переслать. С учетом всего сказанного, в этой главе нам предстоит рассмотреть следующие темы:
внедрение элементов в Web-страницы посредством HTML-тэга