CodeNet / Платформы / Windows / COM/DCOM/COM+
Интеграция COM-компонентов
Несмотря на то, что существует достаточно много информации о программировании с использованием COM, найти четкое описание инструкций по внедрению COM-компонентов в код C++ все же непросто. В действительности большинство документов посвящено разработке компонентов, а не их использованию. В этой статье мы расскажем, как быстро и без лишних затрат использовать готовые компоненты с использованием стандартных возможностей MFC и компилятора Visual C++: директивы #import.
1. Зачем использовать компоненты?
Помимо того, что "сейчас все программируют с использованием компонентов", причин для этого может быть несколько. Применив полезные компоненты сторонних производителей (а таких компонентов на сегодняшний день уже достаточно много), вы сможете заметно расширить возможности вашей программы, не затрачивая времени на разработку собственного кода.
Например, вам нужно, чтобы приложение могло обрабатывать изображения, а вы даже не знакомы с этой предметной областью, чтобы приступить к написанию своего кода. Все, что вам нужно - это приобрести компонент, реализующий нужные вам возможности, затем при помощи описанных в этой статье методов внедрить его в вашу программу. Далее, вы сможете просто вызывать нужные вам методы компонента - и возможно вам не придется писать и строчки кода для обработки графической информации.
У вас также может появиться потребность воспользоваться некоторыми новыми компонентами расширений операционной системы, таких как DirectX Media или ActiveX Data Objects. Или ваши коллеги разработали компонент, который нужно интегрировать в ваше приложения для выполнения всех необходимых операций. В любом случае поставленной цели можно добиться несколькими способами. Один из наиболее быстрых и простых способов мы покажем на практике в нашем тестовом приложении.
Рис. 1. Архитектура "Модель - представление - управление", используемая в тестовом приложении.
2. Методы встраивания компонентов
Чтобы понять прелесть использования MFC и директивы #import для внедрения компонентов, нужно знать возможные пути решения этой задачи.
Например, можно написать код, использующий только возможности C++ и COM, который создаст компонент и будет его использовать; в этом случае вам нужно в подробностях разобраться с концепцией COM-компонентов и написать приличный кусок зачастую повторяющегося и подверженного ошибкам кода. Вы также можете использовать и классы ATL, но, опять же, помимо знания COM вам придется выучить также и ATL. Можно использовать и возможность работы с контейнерами ActiveX, заложенную в мастер создания MFC-приложений, но если вам не хватит возможностей, предлагаемых меню Insert Object, вам придется зарыться в изучение кода поддержки OLE в MFC. Более простой подход - использовать классы-оболочки компонентов, генерируемые библиотекой компонентов и элементов управления (Components and Controls Library). Но эти классы целиком зависят от поддержки автоматизации, которая есть не во всех компонентах. К тому же использование интерфейсов автоматизации может повлечь понижение производительности.
Мы же рассмотрим подход, требующий написания минимального количества кода и минимального знания COM. В нем используется комбинация возможностей MFC и директивы #import. В конце мы рассмотрим, как использовать такой подход для создания компонентов, программного взаимодействия с ними и организации взаимодействия одних компонентов с другими.
3. Основы COM
Если вы хотите эффективно использовать COM-компоненты, вам нужно разобраться в механизме работы и программирования таких компонентов.
В этой статье мы не сможем подробно обо всем этом рассказать, так что давайте хотя бы разберемся в базовых положениях. Первое, что нужно осознать - это то, что возможности компонентов COM реализуются по схеме клиент-сервер. Приложение (или другой компонент) получают доступ к компоненту и используют его возможности в роли клиента, а сам компонент выступает в роли сервера. COM определяет правила взаимодействия клиента и сервера на уровне исполняемого кода, что и позволяет клиентам, написанным на одном языке, взаимодействовать с серверами, написанными на других языках. Клиент взаимодействует с компонентом только посредством интерфейса. Компонент может иметь любое количество интерфейсов, и каждый интерфейс может иметь любое количество определенных в нем методов. Возможности компонента полностью определяются методами его интерфейсов.
Все интерфейсы COM-компонента должны быть унаследованы от интерфейса IUnknown. Существенное различие между интерфейсами заключается в том, поддерживает интерфейс автоматизацию или нет. Интерфейсы с поддержкой автоматизации унаследованы от интерфейса IDispatch, который, в свою очередь, унаследован от IUnknown. Такое наследование влияет на метод доступа к интерфейсу из C++ и других языков, а также определяет, доступен ли интерфейс из таких языков, как JScript или VBScript, которые не могут напрямую манипулировать указателями. Основное следствие наличия автоматизации у интерфейса при использовании C++ - механизм определения возможностей компонента (то есть, какие методы реализованы в данном интерфейсе) во время выполнения. Такая гибкость приводит к проигрышу в производительности, однако, она сильно расширяет совместимость компонента с приложениями, написанными на других языках.
Следует также понимать, каким образом компонент делает доступными свои свойства. Доступ к свойствам компонента позволяет контролировать поведение компонента. Обычно доступ к свойствам осуществляется при помощи методов интерфейса, и эти свойства не обязательно в точности соответствуют внутренним структурам данных компонента, хотя может создаться впечатление, что вы получаете доступ к публичным членам класса.
4. Типы компонентов
Одна из причин отсутствия простого описания COM - наличие большого количества "стандартных" интерфейсов. IUnknown и IDispatch - наиболее распространенные, однако, есть и другие. Одни определяют взаимодействие компонента с контейнером, при размещении компонента в клиентской области окна приложения, аналогично визуальным элементам управления. Другие - работают аналогично элементам управления, размещаемым на Web-страницах, которые взаимодействуют с браузером. В дополнение к этому существует еще и путаница в терминах: COM, ActiveX, OLE, элементы управления и компоненты: Чтобы не путаться во всевозможных вариантах, введем следующую классификацию компонентов: невизуальные компоненты, визуальные компоненты (которые мы будем называть элементами управления) и компоненты, уведомляющие клиента о наступлении событий. Как вы увидите, между этими упрощенными группами возможны пересечения, и существует много других вариантов разбиения компонентов на группы в зависимости от их интерфейсов.
Под невизуальными компонентами мы подразумеваем COM-компоненты, которые не имеют пользовательского интерфейса, что упрощает интеграцию и использование таких компонентов. Обращение к функциональности компонента осуществляется через бинарный интерфейс, удовлетворяющий требованиям COM, что позволяет совмещать приложения и компоненты, написанные на разных языках. Более того, взаимодействие программы и компонента будет выполняться даже в случае, если пользователь установил более новую версию того же компонента, нежели та, которая использовалась при разработке программы. Безусловно, последнее будет выполняться только в том случае, если разработчик компонента строго следовал описанным им соглашениям по бинарному интерфейсу.
Невизуальные компоненты чаще всего предоставляют некоторые вспомогательные функции, например, возможность распознавания речи для голосового управления или вычисление определенных показателей в финансовом приложении. Независимо от того, что делают невизуальные компоненты, процесс их интеграции и использования одинаков. Вы создаете экземпляр компонента, получаете интерфейс, содержащий необходимые вам методы, и вызываете эти методы для выполнения требуемых функций.
Работа с визуальными компонентами, или элементами управления, несколько сложнее, поскольку нужно обеспечить контейнер, содержащий компонент, некоторой информацией о компоненте, так как компонент становится дочерним окном контейнера. Нужно также передавать некоторую информацию и от контейнера обратно компоненту, поскольку последнему нужно знать, как отрисовывать себя и как взаимодействовать с содержащим его контейнером. В приводимом примере используются некоторые возможности класса CWnd библиотеки MFC, автоматизирующие большинство необходимых операций.
Визуальные COM-компоненты чаще называют управляющим элементами ActiveX или OLE (в этой статье мы используем этот термин только по отношению к компонентам, реагирующим на ввод пользователя). Примерами визуальных компонентов могут служить некоторые специфические элементы управления (например, циферблат, шкала) или компоненты, содержащие некоторые графические элементы, например индикатор радара в симуляторе летательного аппарата.
Рисунок 2. Приложение DemoClient в работе
Третий тип компонентов, о которых пойдет речь в этой статье, - это компоненты, уведомляющие клиента через точки взаимодействия (connection points) о наступлении событий (например, о нажатии кнопки мыши или о выполнении определенной вычислительной задачи), либо вызывающие определенные методы клиента для завершения выполнения каких-либо задач (в этом случае клиент обычно тоже является компонентом). В приводимом примере мы увидим, как использовать компоненты в обоих случаях.
Служба COM+ Event Service в Windows 2000 вводит абсолютно новые возможности маршрутизации и обработки сообщений от компонентов. По сравнению с традиционным механизмом, использующим для этих целей точки взаимодействия, новая модель более приспособлена к использованию в распределенных вычислениях и позволяет отделить компоненты от их клиентов при обработке событий. Подробности этого механизма - это отдельная тема для разговора. Мы вернемся к ней в следующих выпусках нашего журнала.
5. Размещение управляющих элементов при помощи CWnd
При размещении элемента управления ActiveX класс CWnd из MFC выполняет огромный кусок работы самостоятельно. Написав одну строку кода, можно попросить CWnd создать элемент управления и выполнить все необходимые действия для обеспечения его взаимодействия с содержащим его контейнером. Еще проще этого добиться при помощи редактора ресурсов, вставив элемент управления в нужное диалоговое окно. Но возможно вам понадобится использовать элементы управления или компоненты в произвольном месте вашего приложения, поэтому мы рассмотрим вариант размещения элемента управления в произвольном окне MFC.
Для добавления управляющего элемента ActiveX в окно вашего приложения в качестве дочернего окна создайте в классе родительского окна член типа CWnd и используйте для создания самого элемента функцию CWnd::CreateControl() там, где это станет необходимо.
Вы можете создавать компоненты на различных стадиях создания вашего приложения, в зависимости от типа компонента. Простые COM-компоненты (без пользовательского интерфейса) можно создавать сразу после инициализации библиотеки COM при помощи вызова AfxEnableControlContainer(). Мастер создания приложения помещает этот вызов в функцию InitInstance() вашего класса, наследованного от CWinApp. Если же ваш компонент имеет пользовательский интерфейс или использует некоторые более сложные возможности ActiveX/OLE, нужно подождать, пока не будут проинициализированы еще несколько объектов окружения MFC. Классы MFC, наследованные от CView, имеют виртуальную функцию OnInitialUpdate(), которую можно переопределить для выполнения необходимых действий непосредственно перед отображением пользовательского интерфейса. Это наилучшее место для инициализации ваших визуальных компонентов, гарантирующее, что MFC уже завершила создание среды OLE.
CWnd::CreateControl() имеет набор параметров, полностью определяющих создаваемый компонент и несущих информацию о клиентском окне, с тем, чтобы Windows могла правильно инициализировать содержащее управляющий элемент окно. Первый параметр - это CLSID или ProgID, идентифицирующий управляющий элемент, который необходимо создать. CLSID и ProgID - это идентификаторы, которые компоненты помещают в реестр Windows при своей инсталляции. CLSID - это GUID (128-битное значение), представляющее класс компонента, а ProgID - текстовое имя, например "DomeWorks.DemoController", которое связано с CLSID в реестре. Функция CreateControl() имеет также другие параметры, включающие название окна, флаги типа окна, координаты прямоугольника в системе координат клиента, в котором должно быть создано окно, родительское окно и идентификатор управляющего элемента. Несколько перегруженных вариантов этой функции видоизменяют эти параметры и имеют значения по умолчанию для дополнительных параметров. Как правило, можно воспользоваться значениями по умолчанию.
6. Использование директивы #import
Директива #import, впервые появившаяся в Visual C++ 5.0,- мощный инструмент для работы с компонентами и их интерфейсами. Ее использование интуитивно более понятно и требует написания гораздо меньшего объема кода, нежели при использовании традиционных возможностей C++/COM. Директива читает библиотеку типов компонента или группы компонентов и создает исходный код классов-оболочек с указателями на интерфейсы (smart pointers), реализованные и экспортируемые из этих компонентов.
Эти классы являются классами-шаблонами, основанными на шаблоне _com_ptr_t, который создает класс-оболочку для COM-интерфейса. Они освобождают вас от необходимости учета ссылок на интерфейс, обязательного при обычных условиях. Они также создают операторы для работы с интерфейсом, использование которых интуитивно более понятно, и позволяют методам интерфейса возвращать значения. В приводимом примере вы сможете увидеть все это в действии.
Вызовы методов COM всегда возвращают результат типа HRESULT, который является набором флагов, содержащих признаки успешности выполнения метода и другую информацию, которую разработчик компонента пожелал закодировать в разрядах HRESULT. Оболочка интерфейса перехватывает значение, возвращаемое методом, и, если оно сигнализирует об ошибке, порождает исключение типа _com_error со значением, содержавшемся в результате метода.
При таком подходе метод-оболочка может вернуть значение параметра из метода интерфейса в качестве своего результата. Вы можете использовать этот результат как параметр в вызове другой функции непосредственно, без необходимости сохранения результата в промежуточной переменной. А исключения автоматически освобождают соответствующие ресурсы, поэтому класс-оболочка сам освободит интерфейсы компонента в случае возникновения исключения. Это освобождает вас от написания значительной части кода обработки ошибок. Опять же, это делает ваш код более понятным и легко читаемым.
Однако есть и некоторые недостатки. Использование исключений делает исполняемый код более громоздким и ухудшает производительность.
Класс-оболочка позволяет вам получать доступ к свойствам интерфейса так, как если бы они были членами самого класса-оболочки. При традиционном подходе, применяемом при программировании COM на C++, для получения значения свойства вам нужно вызвать метод get_XXX(), где XXX - нужное свойство. Аналогично, для изменения значения свойства нужно вызвать метод put_XXX(). При помощи интеллектуальных указателей, используемых в классе-оболочке, вы можете писать программу так, как если бы вы ссылались или изменяли значение свойства напрямую; заботу о вызовах get_ и put_ берет на себя класс-оболочка.
Например, если без использования класса-оболочки вы используете примерно такой код:
HRESULT hr; int i1, i2, result; hr = m_pInterface->get_IntMember(&i1); if (FAILED(hr)) // сделать то, что нужно // освободить интерфейсы // прервать выполнение hr = m_pInterface->CalcOtherInt(&i2); if (FAILED(hr)) // сделать то, что нужно // освободить интерфейсы // прервать выполнение hr = m_pInterface->AddInt(i1, i2, &result); if (FAILED(hr)) // сделать то, что нужно // освободить интерфейсы // прервать выполнение
то при помощи классов-оболочек то же самое можно выполнить вот так:
int i = 5; try { int result = m_pSmartPtr->AddInt(m_pSmartPtr->GetInt(),i); } catch (_com_error &e) { // сделать то, что нужно }
На деле задача может быть не такой простой, как в этом примере, и тогда выигрыш от сохраненного объема кода, предотвращения ошибок и понятности кода будет на много заметнее. То, что интеллектуальные указатели берут на себя все заботы по учету указателей, является основной причиной их использования. Теоретически же их использование может привести к появлению ошибок при работе с указателями, что является основной причиной нестабильности COM-приложений.
7. Компоновка тестовой программы
Для того, чтобы продемонстрировать интеграцию визуальных и невизуальных компонентов, включая те, что используют точки взаимодействия, и те, что взаимодействуют при помощи событий, мы соберем тестовую программу, использующую 3 компонента. Хотя в этой программе используются достаточно простые компоненты, концептуально процедуры работы с ними не отличаются от тех, которые требуются при интеграции "настоящих" компонентов.
В нашей программе используется концепция "модель - представление - управление": программа состоит из трех модулей (в нашем случае - COM-компонентов), выполняющих основные функции приложения. Первый компонент - "модель" - выполняет основную полезную вычислительную нагрузку, преобразуя вводимые данные в результат. Второй компонент - "представление" - занимается отображением работы приложения. Компонент "управление" обеспечивает ввод данных. Такая концепция положена в основу многих приложений, и модель приложения MFC "Документ - представление" - частный вариант использования того же подхода.
В приводимом примере компоненты интегрированы в MFC-приложение с использование возможностей MFC и директивы #import. Приложение создает анимированный компонент представления, скорость анимации и цвет фона в котором контролируются отдельным компонентом с управляющими элементами. "За кулисами" третий компонент обновляет данные компонента представления, переводя установки элементов управления в значения параметров компонента представления. Клиентское приложение, содержащее компоненты, выполняет роль посредника во взаимодействии компонентов, показывая, как можно непосредственно управлять компонентами. Основные связи между компонентами и клиентским приложением показаны на рис. 1.
Создание нашего приложения начнем с мастера приложений MFC для создания программы, которая будет содержать наши компоненты. Назовем ее DemoClient. Все, что нам нужно изменить в параметрах, предлагаемых мастером - это выбрать однодокументный интерфейс для нашей программы. В этом случае для приложения будет использоваться модель "Документ - представление", но мы внесем все необходимые изменения непосредственно в класс CDemoClientView.
7.1 О компонентах
Все компоненты нашей программы собраны вместе с исходным кодом в один проект, поэтому вся информация о компонентах будет собрана в одной библиотеке внутри одного DLL-файла. Поэтому директива #import у нас применяется только к одному файлу. Все три компонента были созданы при помощи ATL, это значительно упростило их создание.
Рис. 3. Новые сообщения и события окна
Компонент, выполняющий роль модели, называется DemoModel. Он выполняет трансляцию установок компонента управления в команды для компонента представления (DemoView). В нем реализован интерфейс, построенный на точках взаимодействия, позволяющий компоненту-модели после начальной инициализации управлять компонентом представления напрямую, без посредничества клиентского приложения. Компонент DemoView использует ATL Composite Control для размещения в нем компонента DirectAnimation. Управляющий элемент используется только как контейнер для компонента DirectAnimation, который заполняет клиентскую область компонента DemoView, выполняя все прорисовки на основании установленных при помощи его интерфейса параметров.
Компонент DemoController - также составной ATL-компонент. Он содержит набор стандартных элементов управления Windows: ползунок, выпадающий список и кнопку. Помимо этого он является источником событий, которые захватываются клиентским приложением и передаются в виде команд компонентам модели и представления.
7.2 Регистрация компонентов
Перед использованием компонентов их нужно зарегистрировать в системе. Обычно регистрация выполняется автоматически при инсталляции программы или на этапе сборки программы при ее разработке в Visual C++. Если вы скомпилируете проект, содержащий демонстрационные компоненты, Visual C++ сам зарегистрирует компоненты на заключительном этапе сборки приложения. Однако, если вам когда-либо понадобится самостоятельно зарегистрировать компонент, содержащийся в библиотеке DLL, вам нужно будет просто запустить утилиту Windows regsvr32.exe и указать в командной строке путь к DLL-файлу. Например, если вы возьмете файл DemoComponents.dll и расположите его в папке C:\MyComponents, вы сможете зарегистрировать его, набрав в командной строке
c:\windows\system\regsvr32 c:\mycomponents\democomponents.dll
(мы подразумеваем, что Windows установлена в каталог C:\Windows).
Можно также отменить регистрацию компонента и удалить записи о нем из реестра, запустив regsvr32.exe с ключом -u, предваряющим путь к библиотеке, содержащей компоненты.
Если вы решили повторить наше исследование демонстрационной программы на своей машине, то сейчас следует откомпилировать проект DemoComponents, чтобы создать и зарегистрировать в системе используемые компоненты.
7.3 Импортирование библиотеки типов
Первый шаг интеграции COM-компонента в приложение: обеспечить приложение достаточным количеством информации для создания компонента. Вы можете сделать это, включив файл заголовков, содержащий глобально уникальные идентификаторы компонента (GUID) и определения классов интерфейсов. Но все же лучше использовать по отношению к библиотеке типов директиву #import, которая сделает то же самое, но кроме этого сгенерирует еще и классы-оболочки для интерфейсов.
Единственным сложным моментом в этом процессе может стать поиск компонентов или библиотек, которые нужно импортировать. Это не составит труда, если вы сами разрабатывали компонент, поскольку библиотека является частью вашего проекта. А вот если вам, к примеру, нужно импортировать библиотеку типов Microsoft Outlook, найти ее не так просто. Один из наиболее простых методов поиска нужной информации заключается в использовании программы OLE/COM Object Viewer, которая входит в комплект поставки Visual C++. Эта программа поможет вам обнаружить компоненты, интерфейсы и библиотеки типов, а также укажет путь к файлам библиотек, если только компоненты были зарегистрированы в системе. Чтобы импортировать информацию о наших компонентах из библиотеки типов, нужно добавить в начало файла заголовков DemoView.h строку
#import "..\DemoComponents\DemoComponents.tlb" no_namespace
Для указания пути используются те же соглашения, что для директивы #include, и в нашем случае подразумевается, что папка проекта DemoComponents содержится в том же каталоге, что и папка проекта DemoClient. Библиотеки типов содержатся либо в отдельном файле TLB, либо в библиотеке DLL. Атрибут no_namespace указывает компилятору игнорировать любое определение пространства имен из библиотеки. Определения в большинстве библиотек заключаются в свои пространства имен с тем, чтобы избежать конфликтов имен с другими определениями в текущем пространстве имен. Директиву no_namespace можно использовать, если вы уверены в отсутствии конфликтов определений; в противном случае можно использовать директиву rename_attribute для переименования конфликтующих определений. Подробнее о всех возможностях можно прочитать в разделе MSDN, посвященном #import.
После того, как вы импортируете информацию из библиотек типов и откомпилируете свой проект, компилятор попытается прочесть информацию из указанного вами файла. Если эта операция выполнится успешно, он сгенерирует классы оболочки, основанные на шаблоне _com_ptr_t, для интерфейсов, определенных в библиотеке типов. Эта информация будет размещена в двух файлах: файле заголовков с расширением TLH и файле реализации с расширением TLI. Эти файлы неявно включаются в ваш проект в области видимости директивы #import, так что вы можете использовать сгенерированные классы. Вы можете также явно подключить файл TLH, если хотите, чтобы сгенерированные классы отображались в окне ClassView. Если вы используете Visual C++ 6.0 или более поздний, информация о сгенерированных классах будет сохранена в проекте. В результате об этих классах узнает IntelliSense и сможет показывать всплывающие подсказки для методов и свойств членов интерфейсов и для параметров методов во время их ввода. Он также добавляет некоторые определения в сгенерированный TLH-файл заголовков с тем, чтобы вы могли использовать имена компонентов и интерфейсов вместо их GUID для таких методов, как CWnd::CreateControl().
Применительно к нашим демонстрационным компонентам, после компиляции проекта будут созданы классы-оболочки для интерфейсов IDemoModelPtr, IDemoViewPtr, IDemoControllerPtr, IDemoModelConnectionPtr и _IDemoControllerEventsPtr. Название каждого сгенерированного класса содержит название интерфейса, к которому добавлено окончание "Ptr". Интерфейсы IDemoModelConnection и _IDemoControllerEvents - исходящие (outgoing), так что это просто базовые классы, в использовании которых напрямую особого смысла нет. Далее мы рассмотрим, как при помощи остальных трех классов-оболочек получить доступ к методам интерфейсов, и как использовать два оставшихся интерфейса.
7.4 Определение членов CDemoClientView
Теперь к нашему классу необходимо добавить несколько членов-переменных, которые будут хранить реализации компонентов и их интерфейсов. Это приведет также к тому, что они будут доступны все время существования представления и вам не придется создавать их заново каждый раз, когда нужно будет использовать их в другом месте кода (например, в функции обработки сообщения).
Сначала добавим в класс CDemoClientView две переменные типа CWnd, которые будут содержать реализации наших двух визуальных компонентов: m_viewWnd и m_controllerWnd. Далее, добавим по одной переменной для каждого класса-оболочки интерфейса, что позволит получать доступ к методам в ответ на сообщения о событиях во время выполнения программы. Добавим по одной такой переменной для интерфейсов IDemoModel, IDemoView и IDemoController. Не забудьте добавлять окончание "Ptr" к названиям интерфейсов, чтобы получить названия классов, сгенерированных директивой #import. Остальные члены, которые необходимо добавить, относятся к механизму точек взаимодействия. Добавим еще две переменные: интеллектуальный указатель на интерфейс IConnectionPoint и ключик типа DWORD. Вот эти определения:
class CDemoClientView : public CView { .... public: CWnd m_viewWnd; // контейнер для компонента представления CWnd m_controllerWnd; // контейнер для компонента управления IDemoModelPtr m_pModel; // интеллектуальный указатель на IDemoModel IDemoViewPtr m_pView; // интеллектуальный указатель на IDemoView IDemoControllerPtr m_pController; // интеллектуальный указатель IConnectionPointPtr m_pCP; // интеллектуальный указатель на IConnectionPoint DWORD m_dwCookie; // содержит ключик для идентификатора // точки взаимодействия... }
Далее, переопределим виртуальную функцию OnInitialUpdate() класса CDemoClientView. Это можно сделать, нажав правую кнопку мыши в окне ClassView и выбрав команду Add Virtual Function, а далее выбрав элемент OnInitialUpdate() из предложенного списка функций. После этого нажмите кнопки Add и Edit, чтобы добавить нужный код и переместиться в определение функции, чтобы внести необходимые дополнения. Здесь, после вызова функции OnInitialUpdate() базового класса, следует вставить код создания наших компонентов.
7.5 Создание компонентов
Теперь мы можем приступить к созданию самих компонентов. Сначала создадим визуальные компоненты, воспользовавшись только что созданными членами типа CWnd. Для этого применим функцию CWnd::CreateControl(). Для указания CLSID компонентов используем результаты работы компилятора при выполнении директивы #import: ключевое слово __uuidof() с именем класса.
Вот код создания визуальных компонентов:
// создание управляющих элементов ActiveX m_viewWnd.CreateControl(__uuidof(DemoView),"View", WS_VISIBLE, CRect(0,0,300,200),this,101); m_controllerWnd.CreateControl(__uuidof(DemoController),"Controller", WS_VISIBLE, CRect(0,201,300,350), this, 102);
Если на этом этапе откомпилировать проект и запустить программу DemoClient, она будет выглядеть примерно так, как показано на рис. 2. Главное окно содержит клиентскую часть, в которой расположены компоненты представления и отображения. Можно даже воспользоваться элементами управления, но это ни к чему не приведет, поскольку мы еще не привязали их изменение к чему-либо.
Следующий шаг - создание компонента-модели. В данном случае оно выполняется несколько проще, поскольку компонент не требует наличия окна, которое бы его содержало. Для создания компонента можно использовать функцию-член CreateInstance() сгенерированного класса-оболочки, передав ей CLSID компонента, который нужно создать. Эта операция пройдет успешно только в случае, если в указанном компоненте реализован интерфейс, класс-оболочка для которого используется. В противном случае интеллектуальный указатель получит значение NULL. Вот как мы создаем этот компонент:
// создание компонента-модели при помощи класса-оболочки m_pModel.CreateInstance(__uuidof(DemoModel));
Теперь, когда мы создали все компоненты, нам необходимы указатели на их интерфейсы, чтобы мы могли вызывать методы этих интерфейсов и выполнять нужные действия. Что касается компонента-модели, то у нас уже есть то, что нужно: компонент был создан при помощи функции CreateInstance() над самим интеллектуальным указателем, так что указатель уже связан с компонентом через интерфейс, который он представляет (в данном случае IDemoModel). Остальные два компонента были созданы при помощи класса CWnd, поэтому их нужно связать с интеллектуальными указателями отдельно. Благодаря специальному члену класса CWnd и оператору связывания класса оболочки эта операция тоже выполняется достаточно просто.
Чтобы это сделать, нужно результат выполнения функции CWnd::GetControlUnknown() передать оператору связывания класса-оболочки. Функция GetControlUnknown() возвращает указатель на интерфейс IUnknown управляющего элемента ActiveX, содержащегося в CWnd. Оператор связывания получает этот указатель и самостоятельно вызывает функцию QueryInterface(), запрашивая указатель на интерфейс того же типа, который представляет класс-оболочка. При успешном выполнении интеллектуальный указатель получит правильное значение указателя на интерфейс нужного типа. Начиная с этого момента можно использовать методы интерфейса, до тех пор, пока указатель не освободит интерфейс перед своим уничтожением. В случае неуспеха вызова QueryInterface() указатель получит значение NULL.
7.6 Создание точек взаимодействия
Ранее мы упоминали, что точки взаимодействия можно использовать для уведомления компонентом клиента о наступлении некоторых событий или запроса к клиенту на выполнение некоторых действий. Однако их можно использовать и для обеспечения взаимодействия между двумя компонентами в одном или разных контейнерах.
Компонент, предоставляющий точки взаимодействия, должен иметь интерфейс IConnectionPointContainer. В своей реализации этого интерфейса компонент указывает, какой именно интерфейс является интерфейсом точек взаимодействия. Клиент должен создать реализацию этого интерфейса и передать компоненту ссылку на эту реализацию. После этого компонент может вызывать методы интерфейса точек взаимодействия. Для реализации интерфейса взаимодействия клиент должен знать, какие методы и свойства этот интерфейс предоставляет. Обычно клиенту доступна эта информация, поскольку компонент определяет интерфейс точек взаимодействия в своей библиотеке типов. Разработчик клиента обычно пользуется этой информацией для того, чтобы определить, какие методы и свойства он должен реализовать для использования точек взаимодействия.
Первая точка взаимодействия, которую мы будем использовать - интерфейс IDemoModelConnection. Он определен в компоненте DemoModel и является исходящим управляющим интерфейсом, при помощи которого компонент-модель может напрямую управлять компонентом представления. Управляющий элемент DemoView создает реализацию этого интерфейса, после чего компонент DemoModel может вызывать методы этого интерфейса. По отношению к общему описанию точек взаимодействия DemoView является клиентом, а DemoModel - компонентом. Следующий код показывает, как подключаются точки взаимодействия:
// создание точки взаимодействия между представлением и моделью IConnectionPointContainerPtr pCPC = m_pModel; IUnknownPtr pUnk = m_pView; if (pCPC != NULL) { pCPC->FindConnectionPoint(__uuidof(IDemoModelConnection), &m_pCP); if (m_pCP != NULL) { m_pCP->Advise(pUnk, &m_dwCookie); } }
Здесь мы снова использовали оператор связывания класса-оболочки для непосредственного вызова QueryInterface() для интерфейса IConnectionPointContainer. То же самое мы делаем для получения указателя IUnknown на управляющий элемент представления. При успешном завершении мы вызываем метод FindConnectionPoint() интерфейса IConnectionPointContainer для компонента модели чтобы узнать, поддерживает ли он интерфейс точек взаимодействия IDemoModelConnection. Если это так, то указатель IUnknown передается компоненту представления, а в ответ принимается значение ключика для этого взаимодействия. После установки взаимодействия компонент DemoModel может вызывать любой метод интерфейса IDemoModelConnection компонента представления. Если вы заглянете в код компонента, то увидите, что модель создает таймер и периодически вызывает метод Update() для обновления угла поворота представления согласно новым показаниям скорости и времени.
7.7 Синхронизация параметров
Последнее, что осталось сделать - синхронизировать параметры компонента управления с компонентами представления и модели. Для этого нужно ввести изменения в функцию OnInitialUpdate():
// инициализация параметров компонентов m_pView->bkgndColor = m_pController->bkcolor; m_pModel->ChangeSpeed(m_pController->speed);
Из этой части кода видно, что интерфейс классов оболочек позволяет использовать свойства как члены-переменные соответствующих классов, получая либо изменяя значения свойств.
Это все, что вам потребуется изменить в функции OnInitialUpdate(), за одним исключением. Весьма полезно заключить приведенные выше строки в блок try: catch, поскольку классы-оболочки с интеллектуальными указателями возвращают результат вызовов методов интерфейсов через параметр HRESULT. Если его значение указывает на ошибку, класс-оболочка порождает исключение типа _com_error. В случае возникновения ошибки ее лучше обработать внутри блока обработки исключений. Для этого нужно разрешить обработку исключений для вашего проекта. В проектах MFC она разрешена по умолчанию, а вот в проектах ATL - нет.
7.8 Обработка событий от компонентов
Элементы управления ActiveX могут быть источниками событий или уведомлений клиенту о возникновении ситуации, на которую клиенту следует обратить внимание. На самом деле события от клиента реализованы при помощи интерфейсов точек взаимодействия. Однако такие события используют передаточный интерфейс, наследованный от IDispatch. Это позволяет использовать их клиентам, в которых реализована автоматизация, например сценариям Web-страниц.
Для подключения механизма событий можно использовать такую же процедуру, как при подключении точек взаимодействия IConnectionPointContainer. Но поскольку интерфейс событий является передаточным, можно пойти более простым путем. Название "передаточный интерфейс" возникло из-за того, что интерфейс содержит механизм передачи вызовов методов соответствующим обработчикам во время выполнения без необходимости явной реализации всех методов интерфейса. Это основной метод обработки событий, и класс MFC CCmdTarget, от которого наследован CWnd, реализует интерфейс IDispatch и может передавать вызовы функциям-обработчикам, определенным в карте событий.
Чтобы использовать эту возможность предков в классе CWnd, нужно сделать 3 вещи. Сначала нужно указать, что ваш класс, унаследованный от CWnd (в нашем случае - CDemoClientView), имеет карту событий, добавив в файле заголовков в определение класса макрос DECLARE_EVENTSINK_MAP(). Далее, нужно добавить в файл реализации (.cpp) код улавливания событий (будет уместно расположить его сразу после макроса карты сообщений, сгенерированного мастером создания приложения):
BEGIN_EVENTSINK_MAP(CDemoClientView, CView) ON_EVENT(CDemoClientView, IDC_CONTROLLER, 1, OnSpeedChanged, VTS_NONE) ON_EVENT(CDemoClientView, IDC_CONTROLLER, 2, OnColorChanged, VTS_NONE) ON_EVENT(CDemoClientView, IDC_CONTROLLER, 3, OnStopStart, VTS_BOOL) END_EVENTSINK_MAP()
Каждый макрос ON_EVENT() определяет передаточный метод, указывая его идентификатор, который был использован в вызове CreateControl(), его DISPID, название функции-обработчика события и все параметры, передаваемые событием, если таковые имеются. Первых два события не передают параметров, на что указывает константа VTS_NONE. Третье передает значение типа Boolean, указывающее, что имеется ввиду: запуск или остановка.
Последний шаг - добавить код обработчиков, которые мы определили. Используйте для создания функций всплывающее меню в ClassView или ClassWizard и добавте функции, приведенные в листинге 1. Как вы можете видеть, все вызовы методов классов с интеллектуальными указателями заключены в блоки try... catch. Код функций достаточно понятен. Глобальная функция dump_com_error() определена для вывода сообщения об ошибке на основании значения HRESULT.
Класс CDemoClientView является посредником между компонентом управления и компонентами модели и представления. Когда компонент управления уведомляет класс CDemoClientView об одном из трех определенных событий, вызывается соответствующая функция. В зависимости от события, класс может получить от компонента управления значение параметра и вызвать метод либо изменить свойство компонентов модели и представления.
7.9 Очистка
Перед тем, как завершить программу, нужно убедиться, что все компоненты были правильно освобождены. Классы-оболочки делают большинство необходимых операций самостоятельно. Когда интеллектуальный указатель-член класса выходит за пределы допустимого диапазона, он освобождает ссылки на интерфейсы, которые он содержал. Когда счетчик ссылок на интерфейс обнуляется, COM узнает, что может уничтожить компонент, когда это будет удобно.
Единственное, что нужно сделать самостоятельно - освободить интерфейсы точек взаимодействия, которые мы также создавали самостоятельно. Для этого нужно, чтобы компонент модели освободил свою ссылку на интерфейс компонента представления при помощи метода Unadvise() интерфейса IConnectionPoint. Именно для этого мы использовали интеллектуальный указатель на этот интерфейс и сохраняли значение ключика, возвращенное методом Advise().
Лучше всего выполнить эти операции тогда, когда окно посылает сообщение WM_DESTROY. Для создания обработчика этого события выберите класс CDemoCleintView в окне ClassView и выберите из всплывающего меню операции Add Windows Message Handler (см. рис. 3). Из предложенного списка выберите сообщение WM_DESTROY и нажмите кнопки Add и Edit. В теле функции OnDestroy() добавьте следующий код перед вызовом CView::OnDestroy() базового класса (а не после него, где мастер приложения расположил комментарий TODO):
try { m_pCP->Unadvise(m_dwCookie); } catch (_com_error &e) { dump_com_error( e ); } CView::OnDestroy();
Этот код передает значение ключика, идентифицирующего интерфейс взаимодействия с компонентом, и освобождает этот интерфейс.
Теперь вы можете откомпилировать и запустить свое приложение (если вы уже создали и зарегистрировали сами компоненты), изменять параметры компонента управления и наблюдать за изменениями в компоненте представления.
Листинг 1. Эти функции-члены обрабатывают события от компонента DemoController, когда пользователь использует элементы управления
// в определении класса CDemoClientView class CDemoClientView : public CView { ... BOOL OnStopStart(VARIANT_BOOL start); BOOL OnColorChanged(); BOOL OnSpeedChanged(); ... } // в файле реализации CDEmoClientView BOOL CDemoClientView::OnSpeedChanged() { try { short speed = m_pController->speed; m_pModel->ChangeSpeed(speed); } catch (_com_error &e) { dump_com_error( e ); } return TRUE; } BOOL CDemoClientView::OnColorChanged() { try { COLORREF color = (COLORREF)m_pController->bkcolor; m_pView->bkgndColor = (long) color; } catch (_com_error &e) { dump_com_error( e ); } return TRUE; } BOOL CDemoClientView::OnStopStart(VARIANT_BOOL start) { try { if (start) m_pModel->Spin(); else m_pModel->Stop(); } catch (_com_error &e) { dump_com_error( e ); } return TRUE; }