Александр Костарев
программист технологического отдела R-Style Software Lab.

Создание элементов управления ActiveX достаточно широко освещается в специальной литературе. Однако методики написания ActiveX-контейнеров рассматриваются в ней гораздо реже, в основном лишь в рамках их взаимодействия с объектами ActiveX. Еще меньше публикаций посвящено процедурам разработки контейнеров, поддерживающих собственный программный интерфейс (API), который обеспечивает работу с ними и содержащимися в них объектами из других приложений или скриптовых языков. Примерами подобных контейнеров могут служить такие программные продукты, как Microsoft Visual Basic, Borland Delphi и т. д.

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

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

Предлагаемые решения основаны на опыте разработки инструментального комплекса RS-Forms - нового программного продукта компании R-Style Software Lab. RS-Forms включает в себя средство разработки графического интерфейса пользователя на платформе Windows, среду исполнения программ, созданных при помощи языков RSL*, С и C++, а также систему отладки RSL-программ.


*Object RSL - язык программирования высокого уровня, созданный в компании R-Style Software Lab. Подробнее см. http://www.softlab.ru/products/rsl/. - Прим. ред.

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

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

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

Fig.1
Рис. 1. Дизайнер форм.

Созданные в дизайнере графические формы можно использовать в любом приложении С/С++, а также из любого скриптового языка, например Visual Basic или RSL. При использовании форм в приложениях C++, разрабатываемых с помощью библиотеки MFC, дизайнер можно применять в качестве редактора диалоговых ресурсов.

А теперь обсудим концепцию построения контейнера и принципы работы с элементами управления ActiveX.

Базовые функции контейнера

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


*Общие вопросы построения контейнеров и серверов COM-объектов рассматриваются в книге Д. Чеппела "Технологии ActiveX и OLE" - М.: "Русская Редакция", 1997.

Fig.2 Рис. 2. Основные компоненты контейнера элементов управления.

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

Объект связи с элементом управления (control site) отвечает за корректное взаимодействие соответствующего элемента с контейнером. Каждый объект связи содержит подобъект, который расширяет элемент управления за счет стандартных для конкретного контейнера наборов свойств, методов и событий. Такой подобъект называется расширенным элементом управления (extended control). Примером расширенных свойств могут служить имя (Name), местоположение в контейнере (Width, Left) и т.п. Указанные наборы - это свойства контейнера, а не какого-либо отдельно взятого элемента управления, хотя именно так это выглядит для пользователя системы. Существует несколько вариантов реализации расширенного элемента управления. Например, он может представлять собой подобъект объекта связи (см. рис. 2) или реальный COM-объект, агрегирующий исходный элемент управления. Каждый из вариантов имеет свои достоинства и недостатки. В этой статье мы рассматриваем только первый способ.

Каждый расширенный элемент управления содержит в качестве подобъекта объект-приемник событий (event sink) от сопоставленного с ним элемента управления (рис. 2). В его задачи входит первичная идентификация полученных событий (требуется пользовательская обработка события или нет) и при необходимости передача их своему объекту-владельцу (extended control), который обеспечивает маршрутизацию событий по иерархии объектов контейнера.

Сценарий создания элемента управления

Внедрение элемента управления в контейнер состоит из трех фаз: создание объекта ActiveX, его инициализация и активизация.

Элементы управления создаются в адресном пространстве контейнера при помощи стандартных функций COM, например CoCreateInstance. В качестве идентификатора элемента управления функции передается соответствующий глобальный уникальный идентификатор класса CLSID. Необходимо отметить, что контейнер должен поддерживать различные варианты идентификации COM-объектов в системе - такие, как идентификатор программы ProgID, уникальный идентификатор класса в виде строки и т.п.

Основное назначение фазы инициализации - передача элементу управления через функцию IOleObject::SetClientSite указателя на интерфейс IOleClientSite объекта связи и вызов функции IPersistStreamInit::InitNew либо IPersistStreamInit::Load (в зависимости от того, загружается объект из хранилища или нет). Передача объекту указателя на интерфейс IOleClientSite может происходить до загрузки/инициализации либо после; момент передачи определяется наличием признака OLEMISC_SETCLIENTSITEFIRST (IOleObject::GetMiscStatus). Это существенно, поскольку от момента передачи указателя зависит, в какой момент времени элемент управления получит значения свойств окружения (ambient properties) от контейнера. Если этот признак проигнорировать, то дальнейшее функционирование объекта ActiveX может быть некорректным.

Затем в рамках рассматриваемой фазы нужно провести начальную инициализацию свойств расширенного элемента управления, дополняющего создаваемый объект ActiveX. К примеру, требуется корректно задать имя объекта (проинициализировать свойство Name, обеспечивающее идентификацию элементов управления в рамках контейнера). Это свойство должен поддерживать любой внедренный в программируемый контейнер элемент управления, но оно тем не менее представляет собой свойство контейнера. Часто по умолчанию объектам присваивают короткое имя класса, к которому они принадлежат (это имя возвращает метод IOleObject::GetUserType для параметра USERCLASSTYPE_SHORT), с добавлением числового индекса, что обеспечивает уникальность имен элементов управления в контейнере. Если получить указанное имя не удается или если оно не удовлетворяет логике контейнера, то можно задать некоторое предопределенное имя с соответствующим индексом.

Активизация элемента управления подразумевает определенную последовательность действий. В первую очередь необходимо установить обратную связь объекта ActiveX с объектом связи в контейнере (control site). Для этого вызывается метод IOleObject::Advise, которому в качестве параметра передается указатель на стандартный интерфейс объекта связи IAdviseSink. Далее требуется корректно запросить интерфейс семейства IViewObject (в соответствии со спецификацией объект ActiveX может поддерживать интерфейсы IViewObject, IViewObject2, IViewObjectEx, состоящие в иерархии наследования) и установить для него обратную связь, вызвав метод IViewObject::SetAdvise с передачей указателя на IAdviseSink. Кроме того, надо сообщить элементу управления имя его контейнера (это реализуется путем вызова метода IOleObject::SetHostName), запросить и сохранить все требуемые для корректной работы с объектом ActiveX интерфейсы (как минимум IOleInPlaceObject и IOleControl). Последнее, что необходимо выполнить для визуального отображения элемента управления, - это вызвать функцию IOleObject::DoVerb* с параметром OLEIVERB_INPLACEACTIVATE.


*В ATL-реализации указанная функция, помимо прочего, отвечает за создание окна для обычных (windowed) элементов управления.

Сценарий удаления элемента управления

Чтобы удалить внедренный в контейнер элемент управления из памяти, требуется исключить из коллекции соответствующий ему объект связи с элементом управления, а затем последовательно выполнить две операции: разрыв обратных связей и освобождение сохраненных указателей на интерфейсы объекта ActiveX.

Для разрыва обратных связей нужно, во-первых, сообщить удаляемому элементу о необходимости освободить (вызовом метода IUnknown::Release) удерживаемые им указатели на интерфейс IAdviseSink объекта связи. Для этого вызываются методы IViewObject::SetAdvise (с передачей в качестве аргумента NULL) и IOleObject::Unadvise, которому нужно предоставить сохраненный на этапе активизации идентификатор связи. Далее требуется активизировать процедуру деинициализации объекта ActiveX (вызвав функцию IOleObject::Close). На следующем шаге элементу управления сообщают о необходимости освободить указатель на интерфейс IOleClientSite, для чего вызывают IOleObject::SetClientSite с параметром NULL.

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

Сценарий сохранения и загрузки

Сохранение объекта контейнера в хранилище, независимо от типа последнего, - это сохранение свойств контейнера (например, свойств окружения) и коллекции внедренных элементов управления, т. е. идентификаторов и свойств (включая расширенные) каждого принадлежащего коллекции объекта. В качестве идентификатора элемента управления может выступать глобальный уникальный идентификатор класса CLSID. Это позволит на стадии инициализации создать требуемый объект ActiveX и загрузить его данными, содержащимися в хранилище после указанного идентификатора. Для сохранения свойств управляющего элемента, например в поток, вызывается метод Save стандартного интерфейса объектов ActiveX IPersistStreamInit. Для загрузки вызывается метод Load того же интерфейса.

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

Интерфейсы коллекции объектов ActiveX

В соответствии со стандартом контейнер элементов управления должен обеспечивать взаимодействие внедренных в него объектов ActiveX. Для этого необходимо поддерживать интерфейс IOleContainer, который позволяет перечислять все вставленные в него элементы управления. При наличии расширенного элемента управления (extended control) перечисление должно проходить именно по его интерфейсам IUnknown, а не по интерфейсам базового объекта.

Для предоставления доступа к коллекции клиентам автоматизации служит стандартный интерфейс коллекции объектов. Стандартная коллекция включает в себя методы Add, Remove, Clear, Item и свойства _NewEnum и Count, обеспечивающие всесторонний перебор элементов. Например, конструкция языка Visual Basic for each использует для перечисления элементов свойство _NewEnum, а конструкция for next предполагает использование свойства Count и метода Item. В языке Object RSL свойство _NewEnum используется при обращении к стандартному методу объекта ActiveX CreateEnum. Это иллюстрирует, например, RSL-программа, распечатывающая с применением указанного метода названия файлов, открытых в приложении Microsoft Excel (ее текст приведен ниже).

import rslx;
ob = ActiveX("Excel.Application", null, true);
en = ob.Workbooks.CreateEnum;

while (en.next)
      println(en.item.Name)
end;

Рассмотренные сценарии позволяют разработать функции добавления элемента управления в контейнер и удаления из него. В большинстве случаев функция добавления создает объект связи с элементом управления (хранящий все необходимые для работы указатели на интерфейсы объекта ActiveX) и вызывает у него аналогичную функцию. Последняя, в свою очередь, осуществляет описанный выше сценарий внедрения (возможно, без фазы активизации). Если формирование объекта ActiveX в оперативной памяти прошло успешно, то функция контейнера добавляет соответствующий ему объект связи в коллекцию. Часто в силу их схожести процедуры внедрения и загрузки элемента управления из хранилища объединяют.

* * *

Мы рассмотрели здесь основные вопросы, связанные с построением контейнеров элементов управления: внедрение, визуальное отображение, сохранение и загрузку объекта ActiveX, а также его корректное удаление из оперативной памяти. Однако в процессе создания графической инструментальной среды нам потребовалось реализовать несколько контейнеров, которые отличались бы друг от друга предоставляемыми интерфейсами автоматизации, подмножествами допустимых для внедрения объектов ActiveX, наборами свойств, методов и событий расширенных элементов управления и т.п. Для этого была предложена модель, позволяющая создавать различные контейнерные элементы и хорошо интегрирующаяся с библиотекой ATL. Независимость от конкретных интерфейсов достигается за счет использования шаблонных классов, параметрами которых и являются эти интерфейсы.


*Например, управляющий элемент Tab представляет собой контейнер страниц свойств, которые являются контейнерами общего назначения.

Эта модель позволяет быстро создавать базовые варианты различных объектов ActiveX, которым присуща "контейнерная логика". Кроме того, реализованная инфраструктура позволяет создавать контейнеры, не являющиеся элементами управления. Такой контейнер можно поместить в качестве окна Windows в любую часть разрабатываемого приложения и затем, посылая соответствующие сообщения, внедрять в него различные элементы управления.

В итоге получилась достаточно гибкая архитектура построения контейнеров, с помощью которой можно создать Мастера (Wizard), расширяющего функциональные возможности среды Microsoft Visual Studio до механизма генерации остова контейнера.

Литература

  1. А. Коберниченко. Visual Studio 6. Искусство программирования. - М.: "Нолидж", 1999.
  2. С. Кубрин. Эволюция RSL - от простейшей генерации отчетов до объектно-ориентированного программирования // RS-Club, 2000, № 3/18/, с. 75-78.
  3. Р. Оберг. Технология COM+. Основы и программирование. - М.: "Вильямс", 2000.
  4. Э. Трельсен. Модель COM и применение ATL 3.0. - СПб.: BHV, 2000.
  5. Д. Чеппел. Технологии ActiveX и OLE. - М.: "Русская редакция", 1997.