Разработка клиента с использованием специальных указателей

Создайте новый пустой проект консольного приложения с именем SayTLibClient и вставьте в него новый файл SayTLibClient.cpp. Введите в файл следующий текст и проследите за тем, чтобы текст директивы #import либо не разрывался переносом ее продолжения на другую строку, либо разрывался по правилам, то есть с использованием символа переноса ' \ ', как вы видите в тексте книги. После этого запустите проект на выполнение (Ctrl+F5):
#import "C:\MyProjects\MyComTLib\Debug\ MyComTLib.tlb" \
no_namespace named_guids
void main()
{
Colnitialize(0);
//====== Используем "умный" указатель
ISayPtr pSay(CLSID_CoSay);
pSay->Say();
pSay->SetWord(L"The client now uses smart pointers!");
pSay->Say();
pSay=0;
CoUninitialize();
}
Несмотря на то что здесь нет многих строчек кода, присутствовавшего в предыдущей версии клиентского приложения, новая версия тоже должна работать. Попробуем разобраться в том, как это происходит.

  • Во-первых, здесь использована директива #import, которая читает информацию из библиотеки типов MyComTLib. tlb и на ее основании генерирует некий код C++. Этот код участвует в процессе компиляции и сборки выполняемого кода клиента. Новый код является неким эквивалентом библиотеки типов и содержит описания интерфейсов, импортированные из TLB-файла.
  • Во-вторых, мы создаем и используем так называемый smart pointer («умный» указатель pSay) на интересующий нас интерфейс. Он берет на себя большую часть работы по обслуживанию интерфейса.

Примечание
Директивой tfimport можно пользоваться для генерации кода не только на основе TLB-файлов, но также и на основе других двоичных файлов, например ЕХЕ-, DLL- или OCX-файлов. Важно, чтобы в этих файлах была информация о типах СОМ-объекте в.
Вы можете увидеть результат воздействия директивы #import на плоды работы компилятора C++ в папке Debug. Там появились два новых файла заголовков: MyCoTLib.tlh (type library header) и MyComTLib.tli (type library implementations). Первый файл подключает код второго (именно в таком порядке) и они оба компилируются так, как если бы были подключены директивой #include. Этот процесс конвертации двоичной библиотеки типов в исходный код C++ дает возможность решить довольно сложную задачу обнаружения ошибок при пользовании данными о СОМ-объекте. Ошибки, присутствующие в двоичном коде, трудно диагностировать, а ошибки в исходном коде выявляет и указывает компилятор. В данный момент важно не потерять из виду цепь преобразований:

  • какая-то часть исходного текста СОМ-сервера (IDL-файл) была сначала преобразована в двоичный код библиотеки типов (TLB-файл);
  • затем на стороне клиента и на основании этого кода компилятор C++ сгенерировал рассматриваемый сейчас исходный код C++ (TLH- и TLB-файлы);
  • после этого компилятор вновь превращает исходный код в двоичный, сплавляя его с кодом клиентского приложения.

Немного позже мы рассмотрим содержимое новых файлов, а сейчас обратите внимание на то, что директива # import сопровождается двумя атрибутами: no_namespace и named_guids, которые помогают компилятору создавать файлы заголовков. Иногда содержимое библиотеки типов определяется в отдельном пространстве имен (namespace), чтобы избежать случайного совпадения имен. Пространство имен определяется в контексте оператора library, который вы видели в IDL-фай-ле. Но в нашем случае пространство имен не было указано, и поэтому в директиве #import задан атрибут no_namespace. Второй атрибут (named_guids) указывает компилятору, что надо определить и инициализировать переменные типа GUID в определенном (старом) стиле: ывю_муСот, CLSiD_CoSay и iio_isay. Новый стиль задания идентификаторов заключается в использовании операции _uuidof(expression). Microsoft-расширение языка C++ определяет ключевое слово _uuidof и связанную с ним операцию. Она позволяет добыть GUID объекта, стоящего в скобках. Для ее успешной работы необходимо прикрепить GUID к структуре или классу. Это действие выполняют строки вида:
struct declspec(uuid("9b865820-2ffa-1Id5-98b4-00e0293f01b2")) /* LIBID */ _MyCom;
которые также используют Microsoft-расширение языка C++ (declspec). Рассматриваемые новшества вы в изобилии увидите, если откроете файл MyCoTLib.tlh:
// Created by Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tlh
//
// C++ source equivalent of Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated file. - DO NOT EDIT!
#pragma once
#pragma pack(push, 8)
#include<comdef.h>
//
// Forward references and typedefs //
struct __declspec(uuid("0934da90-608d-4107
-9eccc7e828ad0928"))
/* LIBID */ _MyCom; struct /* coclass */ CoSay;
struct _declspec(uuid("170368dO-85be
-43af-ae71053f506657a2"))
/* interface */ ISay;
{
//
// Smart pointer typedef declarations //
_COM_SMARTPTR_TYPEDEF(ISay, _uuidof(ISay));
//
// Type library items
//
struct _declspec(uuid("9b865820-2ffa
-lld5-98b4-00e0293f01b2"))
CoSay;
// [ default ] interface ISay
struct _declspec(uuid("170368dO-85be
-43af-ae71-053f506657a2")) ISay : lUnknown
{
//
// Wrapper methods for error-handling
//
HRESULT Say ( ) ;
HRESULT SetWord (_bstr_t word ) ;
//
// Raw methods provided by interface -
//
virtual HRESULT _stdcall raw_Say ( ) = 0;
virtual HRESULT _stdcall raw_SetWord
( /*[in]*/ BSTR word ) = 0;
};
//
// Named GUID constants initializations
//
extern "C" const GUID _declspec(selectany)
LIBID_MyCom =
{Ox0934da90, Ox608d, 0x4107,
{.Ox9e, Oxcc, Oxc7, Oxe8, 0x28, Oxad, 0x09, 0x28} } ;
extern "C" const GUID __declspec(selectany) CLSID_CoSay =
{Ox9b865820,0x2ffa,OxlId5,
{0x98,Oxb4,0x00,OxeO,0x29,Ox3f,0x01,Oxb2}};
extern "C" const GUID __declspec(selectany) IID_ISay =
{
0xl70368dO,Ox85be,0x43af,
{0xae,0x71,0x05,Ox3f,0x50,Охбб, 0x57,Oxa2}
};
//
// Wrapper method implementations //
#include "c:\myprojects\saytlibclient
\debug\MyComTLib.tli"
#pragma pack(pop)
Код TLH-файла имеет шаблонную структуру. Для нас наибольший интерес представляет код, который следует после упреждающих объявлений регистрируемых объектов. Это объявление специального (smart) указателя:
_COM_SMARTPTR_TYPEDEF(ISay, _uuidof(ISay));
Для того чтобы добавить секретности, здесь опять использован макрос, который при расширении превратится в:
typedef _com_ptr_t<_com_IIID<ISay, _uuidof(ISay)> > ISayPtr;
Как вы, вероятно, догадались, лексемы _com_lliD и com_ptr_t представляют собой шаблоны классов, первый из них создает новый класс C++, который инкапсулирует функциональность зарегистрированного интерфейса ISay, а второй — класс указателя на этот класс. Операция typedef удостоверяет появление нового типа данных ISayPtr. Отныне объекты типа ISayPtr являются указателями на класс, скроенный по сложному шаблону. Цель — избавить пользователя от необходимости следить за счетчиком ссылок на интерфейс isay, то есть вызывать методы AddRef и Release, и устранить необходимость вызова функции CoCreatelnstance. Заботы о выполнении всех этих операций берет на себя новый класс. Он таким образом скрывает от пользователя рутинную часть работы с объектом СОМ, оставляя лишь творческую. В этом и заключается смысл качественной характеристики smart pointer («сообразительный» указатель).
Характерно также то, что методы нашего интерфейса (Say и SetWord) заменяются на эквивалентные виртуальные методы нового шаблонного класса (raw_say и raw_setword). Сейчас уместно вновь проанализировать код клиентского приложения и постараться увидеть его в новом свете, зная о существовании нового типа ISayPtr. Теперь становится понятной строка объявления:
ISayPtr pSay (CLSID_CoSay);
которая создает объект pSay класса, эквивалентного типу ISayPtr. При этом вызывается конструктор класса. Начиная с этого момента вы можете использовать smart указатель pSay для вызова методов интерфейса ISay. Рассмотрим содержимое второго файла заголовков MyComTLib.tli:
// Created by Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tli
//
// Wrapper implementations for Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated file. - DO NOT EDIT!
#pragma once
//
// interface ISay wrapper method implementations
//
inline HRESULT ISay::Say ( )
HRESULT _hr = raw_Say();
if (FAILED(_hr))
_com_issue_errorex(_hr, this,_uuidof(this));
return _hr;
inline HRESULT ISay : :SetWord ( _bstr_t word )
{
HRESULT _hr - raw_SetWord(word) ;
if (FAILED (_hr) )
_com_issue_errorex (_hr, this, _ uuidof (this) );
return _hr;
}
Как вы видите, здесь расположены тела wrapper-методов, заменяющих методы нашего интерфейса. Вместо прямых вызовов методов Say и Setword теперь будут происходить косвенные их вызовы из функций-оберток (raw_Say и raw_SetWord), но при этом исчезает необходимость вызывать методы Createlnstance и Release. Подведем итог. СОМ-интерфейс первоначально представлен в виде базового абстрактного класса, методы которого раскрываются с помощью ко-класса. При использовании библиотеки типов некоторые из его чисто виртуальных функций заменяются на не виртуальные inline-функции класса-обертки, которые внутри содержат вызовы виртуальных функций и затем проверяют код ошибки. В случае сбоя вызывается обработчик ошибок _com_issue_errorex. Таким образом smart-указатели помогают обрабатывать ошибки и упрощают поддержку счетчиков ссылок.
Примечание
В рассматриваемом коде использован специальный miacc_bstr_t предназначенный для работы с Unicode-строками. Он является классом-оберткой для BSTR, упрощающим работу со строками типа B.STR. Теперь можно не заботиться о вызове функции SysFreeString, так как эту работу берет на себя класс _bstr_t.
  
Проект на основе ATL
Библиотеки шаблонов, такие как ATL (Active Template Library), отличаются от обычных библиотек классов C++ тем, что они представляют собой множество шаблонов (templates), которые могут и не иметь иерархической структуры. При использовании обычной библиотеки мы создаем класс, производный от какого-то класса из библиотеки и тем самым наследуем всю его функциональность, а значит, и функциональность его предков. С библиотекой шаблонов поступают по-другому. Выбрав шаблон, обращаются к нему для создания нового, класса, .скроенного по образу и подобию шаблона, получая тем самым его общую функциональность. Специфика определяется путем реализации некоторых методов шаблона. Новый класс кроится по шаблону, настраиваемому параметром, который передается в угловых скобках шаблона.
Использование библиотеки ATL полностью снимает с вас заботу о реализации методов ILJnknown, о получении уникальных идентификаторов и регистрации их в системе, а также многие другие рутинные проблемы, связанные с поддержкой технологии СОМ. Вы теперь сможете оценить эти преимущества, так как попробовали создать СОМ-объект с помощью сырых (raw) COM API. У нас нет времени более подробно заниматься технологией СОМ, так как общая направленность книги — использование передовых технологий, а не детальное их изучение. Для получения фундаментальных знаний о технологии мы отсылаем читателя к книгам, перечисленным ранее. Отметим, что текст книги Inside OLE целиком (1200 страниц) помещен в MSDN (см. раздел Books).
Далее рассмотрим, как создать СОМ-объект, обладающий возможностями DLL-сервера (inproc server), Мы создадим новый проект, а в нем остов СОМ DLL-сервера и добавим необходимый нам код, учитывающий специфику СОМ-объекта.

  1. На странице VS Home Page выберите гиперссылку Create New Project.
  2. В окне диалога New Project выберите тип проекта: Win32 Projects, в окне Templates выберите ATL Project, задайте имя проекта ATLGL и нажмите ОК.
  3. В окне мастера ATL Project Wizard выберите вкладку Application Settings и установите переключатель Server Type в положение Dynamic Link Library (сокращенно DLL). Остальные флажки должны быть выключены.
  4. Нажмите кнопку Finish.
Итак, СОМ DLL-сервер или дом для ко-классов готов. Теперь можно начать процесс начинки его классами (или одним классом), которые, в свою очередь, будут являться домами для экспонируемых интерфейсов. Говорят, что ко-класс реализовывает или экспонирует интерфейсы (или один интерфейс). Просмотрите результаты работы мастера. В файле ATLGL.cpp, здесь уже нарушена традиция MFC разделять объявление и реализацию класса, объявлен класс CATLGLModule, скроенный по шаблону и одновременно производный от класса CAtlDllModuleT. К сожалению, документация по ATL содержит весьма краткие сведения о своих классах. Из нее мы можем, однако, узнать, что шаблон классов CAtlDllModuleT поддерживает функциональность DLL-модуля, который

умеет регистрировать себя в качестве такового. Он происходит от класса CAtiModule, у которого есть симметричный потомок CAtlExeModuleT, поддерживающий функциональность ЕХЕ-модуля приложения, и умеет обрабатывать параметры командной строки. Иначе такой модуль называется out-of-proc-сервером (локальным или удаленным сервером). Он выполняется в пространстве собственного процесса, а не клиентского, как в случае in-proc-сервера.
Аналогично MFC-проекту, в котором есть объект theApp, здесь объявлен глобальный объект _AtlModule класса CATLGLModule, унаследованные методы которого позволяют зарегистрировать (DllRegisterServer) в системном реестре наличие нового сервера COM DLL. Но это только начало. Немного позже мы создадим и зарегистрируем СОМ-объект, все его интерфейсы и библиотеку (typelib) упреждающего описания новых объектов COM (coclass, interface, dispinterface, module, typedef). Да, каждый СОМ-объект вносит довольно много записей в системный реестр, поэтому так важно правильно производить обратную процедуру (DllUnregisterServer), иначе реестр превращается в кладбище записей, внесенных объектами, которые уже не существуют в операционной системе.
  
Как работает DLL
Вы уже знаете, что созданный и подключенный компоновщиком динамический модуль система интегрирует в пространство другого (клиентского) процесса, загрузив его по определенному базовому адресу. Любая динамически загружаемая библиотека экспортирует функции, которые пишутся в расчете на то, что их будет вызывать клиентское приложение или другая DLL. Глобальная функция DllMain представляет собой точку входа в динамически подключаемую библиотеку. Она является некоторого рода заглушкой (placeholder) для реального, определяемого библиотекой имени функции. Первый параметр DllMain подан операционной системой и представляет собой Windows-описатель DLL. Его можно использовать при вызове функций, требующих этот описатель, например при вызове GetModuleFileName. Второй параметр указывает причину вызова DLL. Он может принимать одно из четырех значений:

  • DLL_PROCESS_ATTACH — указывает на то, что DLL загружается в виртуальное адресное пространство процесса, так как стартовал сам процесс (неявный вызов DLL) или была вызвана функция LoadLibrary (явный вызов DLL).
  • DLL_THREAD_ATTACH — указывает на то, что текущий процесс создает новый поток (thread). В этот момент система вызывает все DLL, которые уже загружены в пространстве процесса, с тем чтобы они учли новый поток в TLS-сло-тах (Thread Local Storage).
  • DLL_THREAD_DETACH — указывает на то, что поток завершается и DLL может освободить динамические ресурсы, связанные с данным потоком, если они были.
  • DLL_PROCESS_DETACH — указывает на то, что DLL выгружается из адресного пространства процесса либо в результате завершения процесса, либо потому, что процесс вызвал функцию FreeLibrary. В этом случае DLL может освободить память (TLS).

Если DllMain вернет FALSE или 0, то клиентское приложение завершится с кодом ошибки. Характерно, что стратегия работы с СОМ-объектами сходна со стратегией, используемой при работе с DLL. Последняя заключается в том, что каждый вызов функции LoadLibrary увеличивает на единицу счетчик числа пользователей библиотеки. Вызов функции FreeLibrary уменьшает значение счетчика. Обнаружив, что счетчик числа пользователей равен нулю, операционная система автоматически выгрузит ее. Если после этого вызвать какую-либо экспортируемую DLL функцию, то возникнет исключительная ситуация Access Violation, так как код по указанному адресу уже не отображается на адресное пространство процесса.
Возвращаясь к коду, созданному мастером ATL Project wizard, отметим, что кроме DllMain, модуль экспортирует еще 4 функции: DllRegisterServer, DllUnregisterServer, DllCanUnloadNow, DllGetClassObject. Полезно открыть, с помощью окна Solution Explorer файл ATLGL.def, который создал и поместил в папку проекта мастер. Этот файл используется компоновщиком при создании lib-файлов и ехр-файлов, содержащих информацию о DLL и экспортируемых ею функциях. Все эти функции имеют тип STDAPI. На самом деле STDAPI — это макроподстановка, заданная в файле заголовков WinNT.h. С помощью этого файла вы можете самостоятельно расшифровать макрос STDAPI. Он разворачивается (expanded) в такой комплексный описатель:
extern "С" HRESULT _stdcall
Описатель extern «С» означает, что при вызове функция будет использовать имя в стиле языка С, а не C++, то есть описатель отменяет декорацию имен, производимую компилятором C++ по умолчанию.
Примечание
Компилятор C++ использует специальную декорацию имен, для того чтобы отличать overloaded-функции, имеющие одинаковые имена, но разные прото-. типы. Например, вызов: int func(int a, double b); в результате декорации становится: _func@12. Число 12 описывает количество байт, занимаемых списком аргументов. Такая условность называется naming convention (соглашение об именах). Есть и другая конвенция — calling convention (соглашение о связях), которая определяет договоренность о передаче параметров при вызове Win32 API-функций. Описатель _stdcall относится к этой группе. Он определяет: порядок передачи аргументов (справа налево): то, что аргументы передаются по значению (by value), что вызываемая функция должна сама выбирать аргументы из стека и что трансляция регистра символов, верхнего или нижнего, не производится.
Функция DllCanUnloadNow определяет, используется ли данная DLL в данный момент. Если нет, то вызывающий процесс может безопасно выгрузить ее из памяти. Функция DllGetClassObject с помощью третьего параметра (LPVOID* ppv) возвращает адрес так называемой фабрики классов, которая умеет создавать СОМ-объекты, по известному CLSID — уникальному идентификатору объекта.
Откройте файл ATLGLJ.c и.убедитесь, что он пуст. Этот файл будет заполнен кодами компилятором MIDL, о котором мы уже говорили ранее. Запустите приложение (Ctrl+F5). Компилятор и компоновщик создадут исполняемый модуль типа DLL, но загрузчик не будет знать в рамках какого процесса (контейнера) следует запустить его на отладку.
Примечание
В этот момент Studio.Net запросит имя ехе-файла, то есть модуля или процесса в пространство которого должна быть загружена созданная компоновщиком DLL. Вы можете воспользоваться выпадающим списком для выбора строки Browse, которая даст диалог по выбору файла. Найдите с его помощью стандартный контейнер для отладки элементов ActiveX (tstcon32.exe), поставляемый вместе со Studio.Net по адресу:...\MicrosoftVisualStudio.Net\Common7\Tools и нажмите Open, а затем ОК.
В рамках тестового контейнера можно отлаживать работу элементов ActiveX, OLE-controls и других СОМ-объектов. Но сейчас наш объект еще не готов к этому, так как мы не создали СОМ-класса, инкапсулирующего желаемые интерфейсы. Поэтому закройте тестовый контейнер, вновь откройте в рамках Studio.Net уже существующий IDL-файл (Interface Description Language file) ATLGLidl и просмотрите коды, описывающие интерфейс, СОМ-класс и библиотеку типов. Вы помните, что этот файл обрабатывается компилятором MIDL, который на его основе создает другие файлы. Откройте файл ATLGM.c и убедитесь, что теперь он не пуст. Его содержимое было создано компилятором MIDL. В данный момент файл ATLGM.c содержит только один идентификатор библиотеки, который регистрируется с помощью макроподстановки MIDL_DEFINE_GUID.
  
Загадочные макросы
Вернемся в файл ATLGLcpp, где кроме функций, перечисленных выше, присутствуют загадочные макросы. Их смысл довольно прозрачен, но разработчика не должны устраивать догадки, ему нужны более точные знания. Сопровождающая документация, особенно бета-версий, не всегда дает нужные объяснения, поэтому приходится искать их самостоятельно в заголовочных файлах, расположенных по адресу: ...\Microsoft Visual Studio.Net\Vc7\indude или ...\Microsoft Visual Studio.Net\ Vc7\atlmfc\include.
Покажем, как это делается на примере. Нас интересует смысл функциеподобной макроподстановки:
DECLARE_LIBID(LIBID_ATLGLLib)
В результате поиска в файлах по указанному пути (маска поиска должна быть *.h) находим (в файле ATLBase.h), что при разворачивании препроцессором этот макрос превратится в статическую функцию класса CATLGLModule:
static void InitLibldO throw ()
{
CAtlModule::m_libid = LIBID_ATLGLLib;
}
Теперь возникает желание узнать, что кроется за идентификатором LiBiD_ATLGLLib. Во вновь созданном коде файла ATLGM.c находим макрос:
MIDL_DEFINE_GUID(IID,
LIBID_ATLGLLib,ОхЕбОбОЗВС,Ox9DE2, 0x4563,
OxA7,0xAF,Ox8A,Ox8C,Ox4E,0x80,0x40,0x58);
узнав смысл которого мы сможем понять, чем является LiBiD_ATLGLLib. В вашем проекте цифры будут другими, но я привожу здесь те, которые вижу сейчас, для того чтобы быть более конкретным и не загружать вас абстракциями, которых и так хватает. В этом случае поиск не нужен, так как объявление макроса расположено двумя строчками выше. Вот оно:
#define MIDL_DEFINE_GUID(type,name,1,wl,w2,bl,b2,b3,Ь4, \ Ь5,Ьб,b7,b8)
const type name = \ {I,wl,w2, {b1,b2,bЗ,b4,b5,b6,b7,b8}
}
Подставив значения параметров из предыдущего макроса, получим определение LiBiD_ATLGLLib, которое увидит компилятор:
const IID LIBID_ATLGLLib =
{
0xE60605BC, 0x9DE2, 0x4563,
{ 0xA7,0xAF,0x8A, 0x8C,Ox4E, 0x80, 0x40, 0x58 }
}
Отсюда ясно, что LIВID_АТLGLLib — это константная структура типа IID. Осталось узнать, как определен тип данных II D.
В хорошо знакомом файле afxwin.h находим определение typedef GUID IID;. Про Globally Unique Identifier (GUID) сказано очень много, в том числе и в документации Studio.Net. Как мы только что выяснили, изучив работу макросов и LiBio_ATLGLLib, тип IID также используется для идентификации библиотек типов. Система применяет два типа GUID: строковый в реестре, и числовой в клиентских приложениях. Второй макрос, который вы видели в классе
CATLGLModule:
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLGL,
"{E4541023-7425-4AA7-998C-D016DF796716}")
(цифры мои, ваши будут другими) создает строковый GUID. При расширении он превратится в три статические функции класса, две из которых готовят текстовую строку того или иного типа, а третья регистрирует, в случае если bRegister==TRUE, или убирает из реестра эту строку по адресу HKEY_CLASSES_ROOT\APPID\:
static LPCOLESTR GetAppId ()throw ()
{
//====== Преобразование к формату OLE-строки
return OLESTR("{E4541023-7425-4AA7-998C-D016DF796716}") ;
}
static TCHAR* GetAppIdTO throw ()
{
//====== Преобразование к Unicode или char* строке
return _T("{E4541023-7425-4AA7-998C-D016DF796716}") ;
}
// Если bRegister==TRUE, то происходит запись в реестр,
// иначе - удаление записи
static HRESULT WINAPI UpdateRegistryAppId(BOOL bRegister) throw()
{
_ATL_REGMAP_ENTRY aMapEntries [] =
{
{ OLESTRC'APPID") , GetAppIdO }, { NULL, NULL }
};
return ATL::_pAtlModule->UpdateRegistryFromResource( IDR ATLGL, bRegister, aMapEntries);
В данный момент вы сможете найти в реестре свой ключ и ассоциированную с ним строку (ATLGL) по адресу:
HKEY_CLASSES_ROOT\AppID\
{E4541023-7425-4AA7-998C-D016DF796716}
При запуске приложения вышеописанные функции были вызваны каркасом приложения и произвели записи в реестр. Отметьте также, что в реестре появилась еще одна (симметричная) запись по адресу HKEY_CLASSES_ROOT \APPID\ATLGL.DLL. Она ассоциирует строковый GUID с библиотекой ATLGL.DLL. Рассматриваемая строка-идентификатор встречается еще в нескольких разделах проекта, найдите их, чтобы получить ориентировку: в ресурсе "REGISTRY" > IDR_ATLGL (см. окно Resource View) и в файле сценария регистрации ATL.GL.rgs (см. окно Solution Explorer).
Возвращаясь к первому макросу DECLARE_LIBID(LiBiojvTLGLLib), отметим, что скрытая за ним функция initLibid тоже была вызвана каркасом и использована для регистрации библиотеки типов будущего СОМ-объекта. Вы можете найти эту, значительно более подробную, запись по ключу (цифры мои):
HKEY_CLASSES_ROOT\TypeLib\
{E60605BC-9DE2-4563-A7AF-8A8C4E804058}
  
Создание элемента типа ATL Control
Создаваемый модуль DLL будет содержать в себе элемент управления, который внедряется в окно клиентского приложения, поэтому в проект следует добавить заготовку нового СОМ-класса, обладающего функциональностью элемента типа ATL Control. В следующем уроке мы внесем в него функциональность окна OpenGL, поэтому мы назовем класс OpenGL, хотя в этом уроке элемент не будет иметь дело с библиотекой Silicon Graphics. Он будет элементом ActiveX, созданным на основе заготовки ATL. Создать вручную элемент ActiveX достаточно сложно, поэтому воспользуемся услугами еще одного мастера Studio.Net. При включении нового мастера (wizard) важно, где установлен фокус. Отметьте, что сейчас в рабочем пространстве существуют два проекта: один (ATLGL) — это DLL-сервер, а другой (ATLGLPS) — это коды заглушек proxy/stub.

  1. Установите фокус на элемент ATLGL в дереве Solution Explorer и в меню Project выберите команду Add Class (при этом важно, чтобы фокус стоял на имени проекта ATLGL).
  2. В окне диалога Add Class выберите категорию ATL, Templates ATL Control и нажмите кнопку Open.
  3. В окне мастера ATL Control Wizard выберите вкладку Names и в поле Short Name введите OpenGL.
  4. Перейдите на вкладку Attributes и установите следующие значения переключателей и флажков: Control Type: Standard Control, Aggregation: Yes, Threading Model: Apartment, Interface: Dual, Support: Connection Points.
  5. Просмотрите и оставьте по умолчанию установки на вкладке Interfaces. Они сообщают о том, что создаваемый класс будет поддерживать шесть интерфейсов: IDataObject, IPersistStorage, IProvideClassInfoZ, IQuickActivate, ISpedfyPropertyPages и ISupportErrorlnfo.
  6. На вкладке Miscellaneous поднимите флажок Insertable.
  7. На вкладке Stock Properties найдите и добавьте свойство Fill Color, нажав кнопку Add.
  8. Нажмите кнопку Finish.

Просмотрите результаты работы мастера. Самым крупным его произведением является файл OpenGLh, который содержит объявление и одновременно коды класса COpenGL. Для ATL-проектов характерно то, что создаваемые ко-классы наследуют данные и методы от многих родителей, в число которых входят как СОМ-классы, так и интерфейсы. Другой характерной чертой является сосредоточение значительной части функциональности в h-файле. Напрашивается вывод, что некоторые принципы и идеи, отстаиваемые Microsoft в MFC, были инвертированы в ATL. Сколько полемического задора было растрачено в критике множественного наследования (намек на Borland OWL) на страницах документации по MFC, и вот теперь мы видим вновь созданный класс (COpenGL), который имеет 18 родителей, среди которых 5 классов и 13 интерфейсов.
Здесь у вас опять должна закружиться голова, но не сдавайтесь. Важно не выпускать главную нить логики приложения. Резон таков: мастера настрочили уйму кода, который пока непонятен, возможно, и всегда будет таким, но этот код уже работает и нам нужно где-то встроиться в него, чтобы почувствовать возможность управлять общей логикой внедряемого элемента ActiveX. Имея под рукой Wizards Studio.Net, это можно сделать, даже оставаясь в некотором неведении относительно деталей работы интерфейсов СОМ. Вам не придется вручную реализовывать ни одного интерфейса. Вы можете сосредоточиться только на алгоритме работы самого элемента, то есть на том, что вы должны продемонстрировать пользователю вашего объекта.
Запустите приложение, но на этот раз не закрывайте тестовый контейнер, который должен запуститься автоматически, без вашего участия. В окне тестового контейнера вы не увидите признаков нашего элемента, так как он еще не загружен. Дайте команду Edit > IhsertNew Control. После некоторой паузы, в течение которой контейнер собирает информацию из реестра обо всех элементах OLE Controls, вы увидите диалоговое окно с длинным списком элементов, о которых есть информация в реестре.
Примечание
Это совсем не означает, что все элементы живы и здоровы. На мой взгляд, ситуация уже вырастает в серьезную проблему. В систему следует ввести эффективные средства корректировки реестра, потому что совсем неинтересно проводить часы драгоценного времени, копаясь в реестре или инструменте типа OLE/COM Object Viewer (Просмотр объектов OLE/COM) и выясняя, жив элемент или его давно нет. Может быть, как говорят политики, я не владею информацией, но все программки типа CleanRegistry либо опасны, либо мало полезны и неэффективны.
При открытом окне диалога Insert Control вы можете просто ввести букву о — начальную букву нашего элемента OpenGL. Теперь, используя клавиши навигации по списку (стрелки), быстро найдете в нем строку OpenGL Class. Выберите ее и нажмите ОК. Вы должны увидеть окно внедренного элемента, которое выглядит так, как показано на рис. 8.2.
Загляните в файл ATLGLJ.c и увидите три новых макроса типа MIDL_DEFINE_GUID, которые уже выполнили свою работу и поместили в реестр множество новых записей по адресам:
HKEY_CLASSES_ROOT\ATLGL.OpenGL\
HKEY_CLASSES_ROOT\ATLGL.OpenGL.1\
HKEY_CLASSES_ROOT\CLSID\
HKEY_CLASSES_ROOT\ Interface\
Когда клиент СОМ-объекта пользуется услугами локального или удаленного сервера, то есть когда данные передаются через границы различных процессов или между узлами сети, требуется поддержка маршалинга (marshaling). Так называется процесс упаковки и посылки параметров, передаваемых методам интерфейсов через границы потоков или процессов, который мы слегка затронули ранее. Вы помните, что MIDL генерирует код на языке С для двух компонентов: Proxy (представитель СОМ-объекта на стороне клиента) и stub (заглушка на стороне СОМ-сервера). Эти компоненты общаются между собой и решают проблемы Вавилонской башни, то есть преодолевают сложности обмена данными, возникающими из-за того, что клиент и сервер используют различные типы данных — разговаривают на разных языках. Чтобы увидеть проблему, надо ее создать. Интересно то, что при объяснении необходимости этого чудовищного сооружения:

  • idl-файл;
  • новый класс CProxy_iOpenGLEvents в вашем проекте;
  • новый проект ATLGLPS (proxy-stub) в вашем рабочем пространстве;
  • новый тип структур VARIANT, который надо использовать или просто иметь в виду,

приводится соображение о том, что программы на разных языках программирования смогут общаться, то есть обмениваться данными. Как мы уже обсуждали, разработчики имеют в виду четыре языка, два из которых реально используются (Visual C++ и Visual Basic), а два других (VBScript и Visual J++) едва подают признаки жизни. Правда здесь надо учесть бурное развитие нового языка с#, который, очевидно, тоже участвует в движении СОМ.
Откройте файл ATLGLidl и постарайтесь вникнуть в смысл новых записей, не отвлекаясь на изучение языка IDL, который потребует от вас заметных усилий и временных затрат. Прежде всего отметьте, что в библиотеке типов (library ATLGLLib), сопровождающей наш СОМ-объект, появилось описание СОМ-класса
coclass OpenGL
{
[default] interface IQpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
который предоставляет своим пользователям два интерфейса. Я не привожу здесь предшествующий классу OpenGL блок описаний в квадратных скобках, который носит вспомогательный характер. Элементы ActiveX используют события (events) для того, чтобы уведомить приложение-контейнер об изменениях в состоянии объекта в результате действий пользователя — манипуляции посредством мыши и клавиатуры в окне объекта. Найдите описание одного из объявленных интерфейсов:
dispinterface _IOpenGLEvents
{
properties:
methods:
};
Пока пустые секции properties (свойства): и methods (методы): намекают на то, что мы должны приложить усилия и ввести, с помощью инструментов Studio.Net в разрабатываемый СОМ-объект способность изменять свои свойства и экспортировать методы. Информация о втором интерфейсе расположена вне блока, описывающего библиотеку типов:
interface IQpenGL : IDispatch
{
[propput, bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT FillColor([in]OLE_COLOR clr);
[propget, bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT FillColor([out, retval]OLE_COLOR* pclr);
};
  
Двойственные интерфейсы
Технология Automation, ранее известная как OLE Automation, дает совершенно другой способ вызова клиентом методов, экспонируемых сервером, чем тот стандартный для СОМ способ, который мы уже рассмотрели. Вы помните, что он использует таблицу виртуальных указателей vtable на интерфейсы. Automation же использует стандартный СОМ-интерфейс IDispatch для доступа к интерфейсам. Поэтому говорят, что любой объект, поддерживающий IDispatch, реализует Automation. Также говорят о дуальном интерфейсе, имея в виду, что он может быть вызван как с помощью естественного способа (vtable), так и с помощью вычурного способа Automation. Итак, интерфейс IOpenGL предоставляет своим пользователям двойственный (dual) интерфейс.
Dual Interface понадобился для того, чтобы VBScript-сценарий мог использовать СОМ-объекты, созданные с помощью Visual C++. Клиенты, созданные на языке C++, могут с помощью Querylnterf асе получить адрес интерфейса и прямо вызывать его методы, пользуясь таблицей виртуальных функций (vtable), например:
p->SomeMethod(i, d);
В VBScript будут проблемы. Там нет строгого контроля соответствия типов и многие типы C++ ему неизвестны. Интерфейс IDispatch служит посредником в разговоре двух произведений Microsoft. Теперь программа на VBScript может добраться до метода SomeMethod, выполнив длинную цепь вызовов. Сначала она должна получить указатель на интерфейс IDispatch, затем с его помощью (GetiDsOf Names) узнать индекс желаемого метода (типа DISPID — dispatch identifier), на сей раз не 128-битный. После этого она сможет заставить систему выполнить коды метода SomeMethod, но не прямо, а с помощью метода IDispatch: : Invoke, который требует задать 8 параметров, смысл которых может приблизительно соответствовать следующему списку описаний. Последующий текст воспринимайте очень серьезно, так как он взят прямо из справки IDispatch:: invoke:

  • вызовите пятую функцию из vtable, так как IDispatch: : GetIDsOfNames сообщил, что ее имя SomeMethod;
  • возьмите неиспользуемый (пока) параметр;
  • возьмите 32-битный описатель местности (LCID);
  • возьмите флаг DISPATCH_METHOD | DISPATCH_PROPERTYGET, описывающий суть того, что запрашивается у пятой функции;
  • возьмите адрес структуры DISPPARAMS, в которую завернут массив аргументов, массив индексов (DISPID) для них и числа, описывающие размеры массивов;
  • возьмите адрес структуры VARIANT (из 49 полей, правда 47 из них union), в которой пятая функция может возвратить результат вызова, но только если в 4-м параметре (флаге) указано, что результат нужен;
  • возьмите адрес структуры EXCEPINFO, в которую система в случае непредвиденных обстоятельств запишет причину отказа (сбоя);
  • возьмите адрес переменной, в которой вернется индекс первого аргумента в массиве отказов, так как аргументы там хранятся в обратном порядке, а нам нужна ошибка с самым высоким индексом. Но если в HRESULT будет DISP_E_TYPEMISMATCH или DiSP_E_PARAMNOTFOUND, то возвращаемое значение недействительно.
(Поток сознания в скобках, по Джойсу или Жванецкому: новые концепции, новые технологии, глубина мыслей, отточенность деталей, настоящая теория должна быть красивой, тупиковая ветвь?, монополисты не только заставляют покупать, но и навязывают свой способ мышления, что бы ты делал без MS, о чем думал, посмотри CLSID в реестре, видел ли я полезный элемент ActiveX, нужно ли бесшовно

внедрять что-нибудь во что-нибудь, посмотри Interfaces в реестре, что лучше, Stingray-класс или внедренная по стандарту OLE таблица Excel, тонкий (thin) клиент не будет иметь кода, но будет иметь много картинок и часто покупать дешевые сеансы обслуживания, как раньше билеты в кино или баню, если не поддерживать обратную совместимость, то кто будет покупать, лучше не купить, чем перестать играть в DOS-игры, стройный (slim) клиент, хочешь, еще посчитаю — плати доллар, перестань думать, пора работать.)
Дуальные или интерфейсы диспетчеризации (dispinterfaces) в отличие от тех vtable-интерфейсов, с которыми вы уже знакомы, были разработаны для того, чтобы реализовать позднее связывание (late-binding) клиента с сервером. Инструментальная среда разработки Visual Basic в этом смысле является лидером, так как в ней вы почти без усилий можете создать приложение, способное на этапе выполнения, то есть поздно, получить информацию от объекта и пользоваться методами интерфейсов, информация о которых стала доступной благодаря IDispatch.
Стандартные свойства
Возвращаясь к нашему проекту, отметим, что интерфейс юрепсъ предоставляет своим пользователям два одноименных метода FillColor. Первый метод позволяет пользователю изменить (propput) стандартное или встроенное (stock property) свойство: «цвет заливки». Второй — узнать (propget) текущее значение этого свойства. Этот интерфейс был вставлен мастером потому, что при создании элемента мы указали на -необходимость введения в него одного из стандартных свойств. С этой же целью мастер ввел в состав класса переменную:
OLE_COLOR m_clrFillColor;
которая будет хранить значение свойства. Мы должны ею управлять, поэтому давайте зададим начальное значение цвета в конструкторе класса. Найдите его и измените:
COpenGL()
{
m_clrFillColor = RGB (255,230,255);
}
Но этого мало. Для того чтобы увидеть результат, надо изменить коды функции рисования, которую вы найдете в том же файле OpenGLh.
Примечание
Вступив в царство ATL, придется отречься от многих привычек, приобретенных в MFC. Вы уже заметили, что мы теперь вместо char* или CString пользуемся OLESTR, а вместо COLORREF— OLE_COLOR. Это еще не так отвлекает, но вот теперь надо рисовать без помощи привычного класса CDC и вернуться к описателю НОС контекста устройства, которым мы пользовались при разработке традиционного Windows-приложения на основе функций API. Также придется привыкнуть к тому, что описатель HOC hdcDraw упрятан в структуру типа ATL_DRAWINFO, ссылку на которую мы получаем в параметре метода OnDraw класса CComControl.
Напомню, что вся функциональность класса CComControl унаследована нашим классом COpenGL, который, кроме него, имеет еще 17 родителей. Состав полей структуры ATL_DRAWINFO не будем приводить здесь, чтобы не усугублять головокружение, а вместо этого предложим убедиться в том, что можно влиять на облик СОМ-объекта. Особенностью перерисовки СОМ-объекта является то, что он изображает себя в чужом окне. Поэтому, получив контекст устройства, связанный с этим окном, он должен постараться не рисовать вне пределов прямоугольника, отведенного для него. В Windows существует понятие поврежденной области окна (clip region). Это обычно прямоугольная область, в пределах которой система позволяет приложению рисовать. Если рисующие функции GDI попробуют выйти за границы этой области, то система не отобразит этих изменений. Следующий код интенсивно работает с clip region, поэтому для понимания алгоритма рекомендуем получить справку о функциях GetClipRgn и SelectClipRgn. Введите изменения в уже существующее тело функции OnDraw так, чтобы она приобрела вид:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//===== Преобразование RECTL в RECT
RECT& r = *(RECT*)di.prcBounds;
//===== Запоминаем текущую поврежденную область
HRGN hRgnOld = 0;
//== Функция GetClipRgn может возвратить: 0, 1 или -1
if (GetClipRgn(di.hdcDraw, hRgnOld) != 1) hRgnOld = 0;
//====== Создание новой области
HRGN hRgnNew = CreateRectRgn(r.left,r.top, r.right,r.bottom);
// Оптимистический прогноз (новая область воспринята)
bool bSelectOldRgn = false;
//=== Устанавливаем поврежденную область равной г
if (hRgnNew)
{
bSelectOldRgn = SelectClipRgn(di.hdcDraw,hRgnNew) == ERROR;
}
//=== Изменяем цвет фона и обрамляем объект
::rSelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor)); Rectangle(di.hdcDraw, r.left, r.top,r.right,r.bottom);
//=== Параметры выравнивания текста и сам текст
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
//=== Вывод текста в центр прямоугольника
TextOut(di.hdcDraw, (r.left + r.right)/2,
(r.top + r.bottom)/2,
pszText,Istrlen(pszText));
//=== Если был сбой, то устанавливаем старую область
if (bSelectOldRgn)
SelectClipRgn(di.hdcDraw, hRgnOld);
return S_OK;
}
В этой реализации функции OnDraw мы намеренно пошли на поводу у схемы, предложенной в заготовке. Структура RECTL, на которую указывает prcBounds, идентична структуре RECT, но при заливке она ведет себя на один пиксел лучше (см. справку). Здесь это никак не используется. Автору фрагмента не хотелось много раз писать выражение di. prcBounds->, поэтому он завел ссылку на объект типа RECTL, приведя ее к типу RECT. Здесь хочется «взять в руки» CRect, cstring и переписать фрагмент заново в более компактной форме, однако если вы попробуете это сделать, то получите сообщения о том, что CRect и cstring — неизвестные сущности. Они из другого царства MFC. Мы можем подключить поддержку MFC, но при этом многое потеряем. Одной из причин создания ATL была неповоротливость объектов на основе MFC в условиях web-страниц. Мы не можем себе этого позволить, так как собираемся работать с трехмерной графикой. Поэтому надо привыкать работать по правилам Win32-API и классов СОМ.
Тестирование объекта
Вновь запустите приложение и убедитесь в том, что нам удалось слегка подкрасить объект. Теперь исследуем функциональность, которую получили бесплатно при оформлении заказа у мастера.

  1. Поместите курсор мыши внутрь рамки объекта, вызовите контекстное меню и дайте команду OpenGL Class Object. При этом появится диалоговое окно страниц свойств, состоящее из двух станиц (Property Pages).
  2. Сдвиньте окно диалога в сторону, чтобы оно не заслоняло внедренный объект. На первой странице диалога с заголовком Color выберите из списка другой цвет и нажмите кнопку Apply. Цвет должен измениться.
  3. В выпадающем списке Set of colours выберите строку System colours Windows и вновь попытайтесь изменить цвет объекта. На сей раз произойдет осечка.

Попробуем это исправить. Событие, заключающееся в том, что пользователь объекта изменил одно из его стандартных свойств, поддерживаемых страницами не менее стандартного диалога, будет обработано каркасом СОМ-сервера и при этом вызвана функция copenGL: :OnFillColorChanged, код которой мы не трогали. Сейчас там есть только одна строка:
ATLTRACE(_T ("OnFillColorChanged\n"));
которая в режиме отладки (F5) выводит в окно Debug Studio.Net текстовое сообщение. Внесите в тело этой функции изменения:
void OnFillColorChanged()
{
//====== Если выбран системный цвет,
if (m_clrFillColor & 0x80000000)
//====== то выбираем его по индексу
m_clrFillColor=::GetSysColor(m_clrFillColor & Oxlf); ATLTRACE(_T("OnFillColorChanged\n"));
}
Признаком выбора системного цвета является единица в старшем разряде m_clrFillColor. В этом случае цвет задан не тремя байтами (red, green, blue), a индексом в таблице системных цветов (см. справку по GetSysColor). Выделяя этот случай, мы выбираем системный цвет с помощью API-функции GetSysColor. Заодно подправим функцию перерисовки, чтобы убедиться, что объект нам подчиняется и мы умеем убирать лишний код:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//====== Не будем преобразовывать в RECT
LPCRECTL р = di.prcBounds;
//====== Цвет подложки текста
::SetBkColor(di.hdcDraw,m_clrFillColor) ;
//====== Инвертируем цвет текста
::SetTextColor(di.hdcDraw, ~m_clrFillColor & Oxffffff);
//====== Цвет фона
::SelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor));
Rectangle(di.hdcDraw, p->left, p->top, p->right, p->bottom);
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
TextOut(di.hdcDraw, (p->left + p->right)/2,
(p->top + p->bottom)/2,
pszText, Istrlen(pszText)
};
return S_OK;
}
Запустите и убедитесь, что системные цвета выбираются корректно, а перерисовка при изменении размеров объекта не нарушает заданных границ. Некоторые проблемы возникают при инвертировании цвета фона, если он близок к нейтральному (128, 128, 128). В качестве упражнения решите эту проблему самостоятельно.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика