Программисту-профессионалу
PC Magazine/RE logo
©СК Пресс 1996
PC Magazine, April 9, 1996, p. 249

Группы объектов автоматизации OLE

Джон Лэм


Создание группы автоматизированных объектов средствами Delphi 2.0.

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

В предыдущей моей статье ("Reusable Binary Components", PC Magazine, March 12, 1996) детально рассматривалась задача создания связанного списка средствами Си++ для последующего его использования в среде Visual Basic. Обращение к связанному списку - это один из способов организации данных с тем, чтобы быстро добавлять и удалять из него отдельные элементы. В этой статье будет показано, как использовать такой объект связанного списка в среде новой 32 разрядной версии системы Delphi, а также как добиться доступа к содержащейся в нем информации через стандартную обработку групп автоматизированных объектов.

В Visual Basic 4.0, работа которого базируется на использовании механизма VBA (Visual Basic for Applications), содержится ряд дополнительных синтаксических конструкций, предназначенных дл обработки групп объектов. Например, простой цикл перебора по всем элементам в группе под именем MyCollection выглядел бы следующим образом:

For Each Element in MyCollection debug.print Element next

Такие группы могут состоять как из переменных простых типов, так и из сложных объектов. Каждый элемент из MyCollection передается в программу в виде переменной Variant с именем Element. Именно через Element мы получаем доступ к содержащейся в элементе информации. В приведенном примере его данные просто направляются в окно Debug. Но возникает вопрос: как же в действительности средства VBA осуществляют доступ к этой группе объектов и получают из них данные? Для тех, кто хочет разобраться в этом, и предназначена статья. Я же, забегая вперед, скажу, что такой механизм осуществляется через мало известный интерфейс OLE, именуемый IEnumVariant.

Для того чтобы понять особенности работы IEnumVariant и близкого ему интерфейса IDispatch, познакомимся сначала с такими понятиями, как OLE и COM. COM (сокращение от Component Object Model) - это набор стандартных правил, описывающих взаимодействие двоичных объектов между собой. Если в традиционных языках программирования для обеспечения взаимодействия модулей между собой приходится тщательно настраивать их интерфейсные функции, то связь между COM объектами осуществляется через COM интерфейсы. Принято даже соглашение о том, что имя любого COM интерфейса должно начинаться с заглавной буквы I. Далее будут рассмотрены следующие вопросы: как давать определение COM интерфейсам, как протекает их работа и как создавать собственные COM интерфейсы. Рассмотрев пример обработки нашего связанного списка как группового автоматизированного объекта, вы сможете разобраться, как взаимодействуют два основных интерфейса IDispatch и IEnumVariant при создании группового автоматизированного объекта.

Коротко о COM интерфейсе

Все интерфейсы, используемые COM объектами дл "общения" друг с другом, хранятся в виде некоторой таблицы указателей на их функции. По своей структуре она идентична v-таблицам, применяемым дл диспетчеризации виртуальных функций как в языке Си++, так и в Delphi. Поэтому создание COM объектов, как и любых других объектов в этих языках, одинаково просто. Для демонстрации примеров в данной статье была выбрана система Delphi 2.0.

Все COM интерфейсы наследуются из IUnknown, выполняющего три простые функции: AddRef, Release и QueryInterface. Первые две обеспечивают одну очень ответственную для любого COM объекта операцию - подсчет количества обращений. Учитывая, что любой COM объект потенциально может обслуживать много других объектов, необходимо отслеживать, сколько таких объектов взаимодействуют с ним в текущий момент. Функция AddRef служит для приращения значения счетчика обращений, а Release - для его уменьшения. Если этот счетчик становится равным нулю, то функция Release автоматически высвобождает память, отведенную под этот COM объект.

Третья из этих функций - QueryInterface - обеспечивает доступ к другим имеющимся у COM объекта интерфейсам. С помощью QueryInterface любой объект, имеющий указатель на интерфейс IUnknown другого объекта, может в процессе исполнения получить информацию, какие еще интерфейсы доступны этому объекту, а затем соответствующий указатель на любой из них.

Все остальные COM интерфейсы задаются как производные от IUnknown, поэтому должны содержать все три функции интерфейса IUnknown: AddRef, Release и QueryInterface. Покажем на примере, как создать новый COM интерфейс, давая определение нового класса как производного от IUnknown:

type IMyInterface=class(IUnknown); function Foo: Integer; virtual; stdcall; procedure Bar(S: String); virtual; stdcall; end;

Видно, что в новом COM интерфейсе IMyInterface кроме AddRef, Release и QueryInterface, входящих в IUnknown, еще содержатся виртуальные принадлежащие функции Foo и Bar. Подробное описание особенностей работы COM интерфейсов можно найти в литературе, перечисленной в конце этой статьи.

Основа автоматизированных объектов - интерфейс IDispatch

Поскольку основная тема данной статьи - работа автоматизированных объектов, рассмотрим базовый интерфейс OLE Automation - IDispatch. Он представляет собой стандартный COM интерфейс, наследуемый из IUnknown. Его определение на языке Object Pascal приведено в лист. 1.


Лист. 1. Описание интерфейса IDispatch на языке Object Pascal IDispatch = class(IUnknown) public function GetTypeInfoCount(var ctinfo: Integer): HResult: virtual; stdcall; abstract; function GetTypeInfot(itinfo: Integer; lcid: TLCID; var tinfo: ITypeInfo); HResult; virtual; stdcall; abstract; function GetIDsOfNames(const iid: TIID; rgszNames: POleStrList; cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult; virtual; stdcall; abstract; function Invoke(dispIDMember: TDispID; const iid: TIIB; lcid: TLCID; flags: Word; var dispParams: TDispParams; varResult: PVariant; excepInfo: PExeepInfo; argErr; PInteger): HResult; virtual; stdcall; abstract; end;

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

Следующие две функции - GetIDsOfNames и Invoke - это основные "рабочие лошадки" автоматизированных объектов. Для того чтобы разъяснить их роль, рассмотрим простой пример программы на языке VBA. В ней создаетс экземпляр объекта под именем TestObject и вызывается на исполнение его метод GetObjectTitle:

Const MAINCAPTION = 1 Dim myObject as Object, sMainCaption$ Set myObject = CreateObject("TestObject") sMainCaption = MyObject.GetObjectTitle(MAINCAPTION)

Новый экземпляр объекта TestObject создаетс предложением в третьей строке примера. Передаваемый функции CreateObject аргумент TestObject распознаетс ею как параметр ProgID (сокращенное название идентификатора Program ID). Фактически это - просто строка, которую можно прочитать и с помощью которой можно дать однозначное определение объекту автоматизированному. Используя ProgID, VBA обращается к системному реестру в поиске идентификатора GUID (globally unique identifier - уникальный идентификатор глобального уровня), относящегося к TestObject. Затем VBA производит вызов CoCreateInstance - библиотечной функции OLE - для фактического создания нового экземпляра TestObject и получения указателя на его интерфейс IUnknown. Обращаясь к функции QueryInterface интерфейса IUnknown этого нового объекта, VBA может получить доступ к его интерфейсу IDispatch. Поскольку TestObject - автоматизированный объект, рассматриваема функция передает в вызывающую программу указатель на его интерфейс IDispatch, и VBA присваивает это значение переменной myObject.

Однако самое интересное начинается, когда мы обращаемся к методу GetObjectTitle объекта TestObject. Выглядит это как простой вызов функции, но в действительности VBA приходится проделывать достаточно много скрытой от нас работы. Рассмотрим для начала наиболее простой вариант обращения к автоматизированному объекту. VBA вызывает функцию GetIDsOfNames, передавая ей в качестве аргумента значение GetObjectTitle. Если объект TestObject действительно имеет принадлежащую функцию GetObjectTitle (учитывая, что в состав TestObject входит функция GetIDsOfNames, этот вопрос будет обязательно решен!), return-значение функции GetIDsOfNames будет идентификатором диспетчеризации Dispatch ID (DispID) для переданного значени GetObjectTitle.

Используя DispID, VBA через функцию Invoke обращается к GetObjectTitle. Аргументы для метода GetObjectTitle передаются как массив Variant переменных, указатель на который записывается в параметр dispParams функции Invoke. В нашем примере массив Variant переменных будет содержать лишь единственный элемент - переменную MAINCAPTION, имеющую тип Integer. Значение, передаваемое при возврате управления методом GetObjectTitle, записывается в параметр varResult функции Invoke в виде строковой переменной Variant. Затем VBA присваивает ее переменной sMainCaption. Мы рассмотрели наиболее простой случай взаимодействия VBA с автоматизированным объектом. Однако если объект тоже содержит библиотеку типов, то взаимодействие носит более сложный характер.

Установка связей через библиотеку типов

В настоящее время существует несколько вариантов управления механизмом автоматизации OLE различной степени сложности. Наиболее развитая из таких систем - Visual Basic 4.0. Благодаря чему система VB4 использует уже упоминавшуюся нами функцию GetTypeInfo интерфейса IDispatch. В результате VB4 за один проход получает полный список имен и типов параметров для всех методов и параметров автоматизированного объекта. Иными словами, у VB4 отпадает необходимость обращаться к функции GetIDsOfNames для извлечения перечн идентификаторов DispID, необходимых для работы функции Invoke. Это дает значительный прирост в быстродействии по сравнению с другими вариантами управлени автоматизацией OLE.

Однако даже это не предел - можно добиться еще более высокого быстродействия. Любой объект управлени автоматизацией, где используются библиотеки типов, может работать с так называемыми двойными ("dual") интерфейсами, которым свойственны высокое быстродействие интерфейса v-таблиц и гибкость позднего связывания, характерная для интерфейса IDispatch. Первые семь принадлежащих функций у "двойного" интерфейса те же, что и у IDispatch. Однако перечень принадлежащих функций "двойного" интерфейса этим не ограничивается. Начиная с восьмой позиции, в v-таблице содержатся обращения к методам автоматизации, которые вызываются через функцию Invoke. При чтении библиотеки типов некоторого объекта Automation управляющий объект получает информацию о том, какие аргументы необходимы для функций, содержащихся в v-таблице. Благодаря этому управляющий объект может вызывать эти функции непосредственно через обращения, содержащиеся в v-таблице, обходясь без функции Invoke. Такой подход дает еще одно преимущество: нет необходимости передавать аргументы как Variant-переменные, что обеспечивает дополнительный выигрыш в производительности. В настоящее время единственным вариантом механизма управления автоматизацией, работающим с двойными интерфейсами, является система VB4.

Параметры объекта автоматизации

Следующий важный для понимания вопрос - в каком виде содержится информация в параметрах объекта автоматизации. Как уже упоминалось, основной вид хранения данных для таких объектов - переменные с типом Variant. Тип Variant представляет собой блок данных длиной 16 байт, определение которого приведено в лист. 2.


Лист. 2. Определение типа данных Variant TVariantArg = record vt: TVarType: wReserved1: Word; wReserved2: Word; wReserved3: Word; case Integer of VT_UI1: (bVal: Byte); VT_I2: (iVal: Smallint); VT_I4: (lVal: Longint); VT_R4: (fltVal: Single); VT_R8: (dblVal: Double); VT_BOOL: (vbool: TOleBool); VT_ERROR: (scode: HResult); VT_CY: (cyVal: TCurrency); VT_DATE: (date: TOleDate); VT_BSTR: (bstrVal: TBStr); VT_UNKNOWN: (unkVal: IUnknown); VT_DISPATCH: (dispVal: IDispatch); VT_ARRAY: (parray: PSafeArray); VT_BYREF or VT_UI1: (pbVal: ^Byte); VT_BYREF or VT_I2: (piVal: ^Smallint); VT_BYREF or VT_I4: (plVal: ^Longint); VT_BYREF or VT_R4: (pfltVal: ^Single); VT_BYREF or VT_R8: (pdblVal: ^Double); VT_BYREF or VT_BOOL: (pbool: ^TOleBool); VT_BYREF or VT_ERROR: (pscode: ^HResult); VT_BYREF or VT_CY: (pcyVal: ^TCurrency); VT_BYREF or VT_DATE: (pdate: ^TOleDate); VT_BYREF or VT_BSTR: (pbstrVal: PBStr); VT_BYREF or VT_UNKNOWN: (punkVal: ^IUnknomn); VT_BYREF or VT_DISPATCH: (pdispVal: ^IDispatch); VT_BYREF or VT_ARRAY: (pparray: ^PSafeArray); VT_BYREF or VT_VARIANT: (pvarVal: PVariant); VT_BYREF: (byRef: Pointer); end ;

Большинство данных типа Variant передаются по значению (by value), т. е. они целиком содержатся в Variant переменной. Однако имеется одно важное исключение - строковые переменные. Используемые дл объектов автоматизации строки носят особое название - Basic строки или сокращенно BSTR. Это гибрид строки, принятой в языке Pascal, и строки с нулевым символом в конце, как принято в стиле языка Си (рис. 1).

Рис. 1. Структура Basic строки (BSTR)
______________________________________________________ | Длина строки | Последовательность символов | \0 | |______________|_________________________________|_____| ^ | LPCSTR

Достоинство BSTR заключается в том, что они обеспечивают возможность быстрого объединения и просмотра содержимого строк, что характерно для формата Pascal, поскольку длина строки хранится в ней же. Имеется другое достоинство - универсальность, поскольку эти "встроенные" строки формата, свойственного языку Си, можно передавать функциям API, принимающим именно строки, которые оканчиваются нулевым символом.

Возможность использования строк переменной длины исключительно удобна для программистов. Однако такое удобство иногда влечет за собой определенные проблемы. Строковая Variant-переменная содержит указатель на адреса памяти, выделенные под BSTR. Поскольку память, выделенная под строковую Variant переменную, будет использоваться для параметров, передаваемых между объектами, возникает вопрос: какой же из этих объектов владеет памятью, выделенной под строку? Объект, который производит вызов, или объект, который получает эти параметры? По принятому соглашению именно на получающий объект возлагается ответственность за высвобождение памяти, занятой строкой, после того как все манипуляции над ней завершены. Но как один объект может высвобождать память, которая была выделена совсем другим объектом?

Для решения этой проблемы, связанной с правилами владения памятью, OLE предлагает целый набор функций API, осуществляющих выделение, обработку и высвобождение памяти под BSTR. Поэтому программистам следует придерживаться лишь одного простого правила: вызывающий объект выделяет память для строки, обращаясь к SysAllocString; вызываемый объект обращается к функции SysFreeString после завершения всех манипуляций с этой строкой. Обе эти функции относятся к Windows API и определены в модуле OLEAUT32.DLL.

Групповые объекты автоматизации

Разобравшись со всеми этими вопросами, мы приступаем к изложению основной цели данной статьи: использованию группы автоматизированных объектов OLE. Как уже было отмечено, у любого автоматизированного объекта должен быть интерфейс IEnumVariant. С его помощью объект управления автоматизацией реализует синтаксическую конструкцию "For Each...In..." для последовательного перебора всех отдельных элементов, содержащихся в таком групповом объекте. Обслуживание интерфейса IEnumVariant осуществляется перечисляемым объектом. Перечисляемые объекты - это самостоятельные объекты, создаваемые групповым объектом; именно с их помощью объект управления автоматизацией может перебирать элементы группового объекта. Рассмотрим принадлежащие функции интерфейса IEnumVariant. Основные из них - это Reset и Next. Функция Reset просто устанавливает перечисляемый объект в исходное состояние на начало группового объекта; функция Next передает в качестве результата последующие n элементов этого группового объекта.

Когда управляющий объект автоматизацией запрашивает указатель на интерфейс IUnknown перечисляемого объекта, именно групповой объект осуществляет его передачу. Каким же образом управляющий объект организует запрос о перечисляемом объекте группового объекта? Делается это через специально зарезервированное значение идентификатора DispID, которое передается в параметр _NewEnum автоматизированного объекта. Поскольку управляющий объект всегда обращается к параметру _NewEnum со значением DispID, равным -4, будет ли действительно вызван параметр _NewEnum, особого значения не имеет.

Получив запрос на чтение _NewEnum, объект автоматизации создает новый экземпляр перечисляемого объекта и передает обратно указатель на его интерфейс IUnknown. С его помощью объект управления вызывает функцию QueryInterface с запросом на интерфейс IEnumVariant. Получив требуемый указатель, управляющий объект должен применить к IUnknown функцию Release, поскольку больше этот интерфейс ему не понадобится. Вызов этой функции управляющим объектом в данном случае очень важен, так как перечисляемый объект будет существовать до тех пор, пока его счетчик обращений не достигнет нуля.

После этого управляющий объект будет циклически обращаться к функции Next интерфейса IEnumVariant используемого перечисляемого объекта, до тех пор пока передаваемое ему return-значение равно S_OK. Кажда Variant переменная, получаемая от функции Next, присваивается параметру Element, в цикле "For Each Element in Collection". Эта Variant переменная может содержать либо данные, либо другой автоматизированный объект. Каким образом это удается обеспечить? Просто существует особый тип Variant переменных, содержащих указатель на интерфейс IDispatch.

Если VBA обращается к автоматизированному объекту и получает в ответ Variant переменную с указателем на IDispatch, то параметр Element будет обрабатываться как переменная, содержащая другой объект. Это обеспечивает компоновщику группового объекта возможность создавать наборы объектов. Для примера назовем Microsoft Excel, который умеет объединять групповые объекты WorkSheet в состав единого группового объекта Worksheets.

Интерфейс IEnumVariant и его компонентные функции
Функция Назначение
Next Передать последующие n элементов, содержащиеся в групповом объекте
Skip Передвинуться на n элементов вперед в групповом объекте
Reset Выставить перечисляемый объект на начало группового объекта
Clone Создать копию текущего перечисляемого объекта с учетом его состояния

Обработка перечисляемого объекта управляющим объектом завершается после получения от функции Next значения S_FALSE. Если это случилось, вызываетс принадлежащая функция Release интерфейса IEnumVariant, которая высвобождает память, занятую перечисляемым объектом, если его счетчик обращений достиг значения, равного 0. Теперь, когда мы разобрались во всех этапах жизненного цикла перечисляемого объекта, рассмотрим вопрос, как его создать.

Delphi 2.0 и создание автоматизированных объектов

Новая 32 разрядная версия системы Delphi носит название Delphi 2.0. Среди многих ее достоинств можно назвать средства для работы с автоматизацией OLE: это и объект управления этим механизмом, и средства дл создания самих объектов. Если потребуется создать автоматизированный объект в среде Delphi 2.0, то особых трудов на это не потребуется. Для примера рассмотрим, как организовать в Delphi 2.0 взаимодействие с объектом, представляющим собой связанный список и созданным нами средствами Visual Си++ 4.0.

Для того чтобы сформировать автоматизированный объект, следует просто дать определение новому классу как наследуемому из TAutoObject. Покажем соответствующую часть текста программы нашего нового объекта связанного списка:

type TVBList = class(TAutoObject); automated property _NewEnum: Variant read GetNewEnum dispid -4; procedure AddHead(const S: String); function GetNext: String; ... end;

Теперь все определения для методов (процедур и функций) и параметров, следующих после новой директивы automated, представляют собой соответствующие методы и свойства механизма автоматизации. Напомним, что все аргументы передаются между управляющим объектом и самим автоматизированным объектом как Variant-переменная. Задача преобразования типов между Variant и типами, используемыми в Delphi, осуществляется средствами самой среды Delphi до и после обращения к методам и параметрам соответствующего объекта. Создание разработчиком методов и функций автоматизированного объекта происходит точно так же, как и создание методов и функций любого другого объекта. Это - гораздо более элегантный вариант решения проблемы преобразовани типов параметров, нежели использование макроконструкций и подключение "Мастера преобразования классов" (Class Wizard), как это делается для достижения аналогичной цели при работе с MFC (Microsoft Foundation Classes).

А как же обстоят дела со связанным списком? Напомню, о чем говорилось в предыдущей статье: при создании связанного списка в MFC использовался объект CStringList. В результате разработка свелась к созданию циклического класса автоматизированных объектов CStringList. В Delphi имеется аналогичный "списочный" объект под именем TStringList; однако в отличие от первого это - уже не связанный список, а массив, имеющий специальную функцию для вставки в него элементов. Для того чтобы упростить задачу, я решил сохранить объекту Delphi VBList те же интерфейсные функции, что и у объекта Си++ VBList. Однако учтите, что быстродействие объекта Delphi VBList при выполнении вставки элементов будет ниже, чем у объекта Си++ VBList, поскольку один из них является массивом, а другой - связанным списком.

Создание интерфейса IEnumVariant

Процесс создания перечисляемого объекта также не вызывает особых затруднений. Сначала дается определение интерфейса IEnumVariant как производного от IUnknown. Как это делается, - показано в лист. 3.


Лист. 3. Фрагмент программы для объекта IEnumVariant // Объект IEnumVariant // // Создать новый экземпляр перечисляемого объекта, // передав ему значение ссылки на объект TStringList из // объекта TVBList constructor IMyEnumVariant.Create(pStringList: TVBList); begin m_pVBList := pStringList; m_CurrentPosition := -1; FRefCount := 1; end; // Передать последующие celt элементов группового объекта function IMyEnumVariant.Next(celt: Integer: elt: PVariantArgList: pceltFetched: Integer): HResult; var hr: HResult; I: Integer; OleStr: TBStr; begin // Проверить допустимость значений аргументов, полученных // от управляющего объекта pceltFetched := 0; if pceltFetched <> 0 then pceltFetched := 0 else if celt > 1 then begin Result := E_INVALIDARG; Exit ; end; // Инициализировать массив Variant-переменных for I :=0 to celt -1 do VariantInit(Variant(elt^[I])); hr := S_OK; // Теперь попытаться передать запрашиваемое число элементов for I := 0 to celt - 1 do begin if m_CurrentPosition < m_pVBList.Count then begin m_pVBList.SetIndex(m_CurrentPosition); OleStr :=StringToOleStr(m_pVBList.GetCurrent); PVarData(@elt^[I])^.VType := varOleStr; PVarData(@elt^[I])^.VOleStr := OleStr; Inc(m.CurrentPosition); Inc(pceltFetched); end else hr := S_FALSE; end; Result := hr; end: function IMyEnumVariant.Skip(celt: Longint): HResult; begin end; //Выставить текущий указатель списка на его начало function IMyEnumVariant.Reset: HResult; begin m_CurrentPosition := 0; Result := S_OK; end; function IMyEnumVariant.Clone(var enum: IEnumVariant): HResult; begin end; // Стандартный вариант интерфейса IUnknown function IMyEnumVariant.QueryInterface(const iid: TIID; var obj): HResult; begin if IsEqualIID(iid, IID_IUnknown) or IsEqualIID(iid, IID_IEnumVariant) then begin Pointer(obj) := Self; AddRef; Result := S_OK; end else begin Pointer(obj) := nil; Result := E_NOINTERFACE; end; end; function IMyEnumVariant.AddRef: Longint; begin Inc(FRefCount); Result:= FRefCount; end; function IMyEnumVariant.Release: Longint; begin Dec(FRefCount); Result := FRefCount; If FRefCount = 0 then Free; end // Собственное определение интерфейса IEnumVariant IMyEnumVariant = class(IUnknown) constructor Create(pStringList: TVBLlst); // Функции стандартного интерфейса IUnknown function QueryInterface(const iid: TIID; var obj): HResult; override; function AddRef: Longint; override; function Release: Longint; override; // Функции интерфейса IEnumVariant function Next(celt: Integer; elt: PVariantArgList; pceltFetched: Integer): HResult; virtual; stdcall; function Skip(celt: Longint): HResult; virtual; stdcall; function Reset: HResult; virtual; stdcall; function Clone(var enum: IEnumVariant): HResult; virtual; stdcall; private m_pVBLlst: TVBList; m_CurrentPosition : Integer; FRefCount: Integer;

Затем подготавливается собственный вариант принадлежащих функций AddRef, Release и QueryInterface интерфейса IUnknown, используемых вместо стандартных; их абстрактные описания, содержащиеся в IUnknown, заменяются на новые. Теперь - очередь виртуальных функций интерфейса IEnumVariant. Особое значение имеет порядок, в котором даются определения этих функций. Причина в том, что все они вызываются управляющим объектом автоматизации из v-таблицы данного интерфейса с учетом их относительного положения внутри нее. Достаточно подробное описание COM интерфейсов можно найти в документации OLE SDK или в файле OLE2.INT, входящем в комплект документации Delphi.

Но возникает вопрос, как же реально создаетс перечисляемый объект? Помните, мы говорили, что управляющий объект Automation регистрирует запрос на параметр _NewEnum. Мы объявляем этот параметр с помощью директивы dispid, чтобы присвоить параметру DispID значение, равное -4. В ответ будет вызываться следующа функция GetNewEnum:

function TVBList.GetNewEnum: Variant; begin // создать объект на ходу FEnumVariant := IMyEnumVariant.Create(Self); VarClear(Result); TVarData(Result).VType := varUnknown; TVarData(Result).VUnknown := FEnumVariant; FEnumVariant.Reset; end;

В процессе работы данная функция "на ходу" создает новый перечисляемый объект и передает ссылку на объект VBList его конструктору. Функция GetNewEnum передает указатель на интерфейс IUnknown созданного перечисляемого объекта и с помощью функции Reset устанавливает указатель на начало списка. Конструктор Create интерфейса IMyEnumVariant передает указатель на объект VBList и присваивает счетчику обращений данного перечисляемого объекта значение 1.

Теперь рассмотрим, как работают AddRef и Release - функции перечисляемого объекта, предназначенные дл подсчета обращений. Функция AddRef просто наращивает счетчик и передает его новое значение. Функция Release уменьшает его значение, а затем проверяет, достигло ли количество обращений нуля. Если да, то вызываетс собственный метод высвобождения памяти, отведенной под перечисляемый объект.

Теперь более подробно рассмотрим работу функции QueryInterface. Интерфейсы IUnknown и IEnumVariant перечисляемого объекта имеют одинаковый набор функций. Поэтому функция QueryInterface сначала проверяет значение iid - идентификатор интерфейса, чтобы узнать, какой из этих интерфейсов запрошен вызывающим объектом: IUnknown или IEnumVariant. Если запрошен один из них, то перечисляемый объект передает свой указатель и увеличивает значение собственного счетчика обращений на единицу. Если запрашивается любой другой интерфейс, то перечисляемый объект передает значение ошибки E_NOINTERFACE, сообщающее вызывающему объекту, что такого интерфейса нет.

Функция NEXT!

Наиболее важная функция интерфейса IEnumVariant - Next. Именно эту функцию циклически вызывает управляющий объект автоматизации при переборе элементов в групповом объекте. Управляющий объект может запросить один или несколько элементов группового объекта и на функцию Next возлагается задача передать указанное количество элементов. Если это осуществимо, то они передаются через параметр elt - массив переменных с типом Variant. Если количество элементов в групповом объекте меньше запрашиваемого, то в этом случае все они передаются вызывающему объекту, а параметру pceltFetched присваивается реальное значение количества переданных элементов. Если число элементов в групповом объекте больше запрашиваемого, то функция принимает значение S_OK; в противном случае - S_FALSE, информирующее о том, что достигнут конец группового объекта.

Теперь разберемся, как функция Next извлекает информацию из объекта VBList. Для этих целей используется переменная m_CurrentPosition, служаща текущим индексом в объекте VBList. Поэтому, для того чтобы извлечь из VBList текущий элемент, сначала с помощью функции SetIndex список VBList позиционируетс по текущему значению его индекса. Затем, чтобы извлечь эту строку, производится вызов принадлежащей функции GetCurrent. Учтите, что все строковые параметры необходимо преобразовать к типу BSTR. Для осуществлени этой операции вызывается функция StringToOleStr. Две следующие команды задают значение Variant переменной как переменную с типом BSTR, установив указатель в поле VOleStr этой Variant переменной на только что подготовленную нами BSTR строку.

Тестовый пример

Теперь, когда все работы по созданию VBList - группового автоматизированного объекта - завершены, можно составить маленькую тестовую программу. В лист. 4 показан простой пример программы на языке VB4. Сначала она добавляет объекту VBList несколько элементов, а затем выводит содержимое этого объекта в элементе управления окном списка, пользуясь для этого синтаксической конструкцией "For Each Element in Collection".


Лист. 4. Пример программы на языке VBA, где используется синтаксическая конструкци "For Each Element in Collection" Private Sub Commandl_Click() Call tmAddToList(CStr(Textl.Text)) Text1.Text = "" Text1.SetFocus End Sub Private Sub Command2_Click() Dim objListReference As Object. i% ' Получить ссылку на список строк, созданный в этом модуле Set objListReference = tmGetList() List1.Clear ' Теперь, используя полученную ссылку, прочитать ее ' элементы и 'заполнить ими объект "Список просмотра" ' (ListBox) For Each Element In objListReference List1.AddItem Element Next End Sub Dim m_StringList As Object Sub Main() ' Создать постоянный объект под список Set m_StringList = CreateObject("VBLIST.VBList") frmTest.Show 1 Set m_StringList = Nothing End Sub Sub tmAddToList(strValue$) m_StringList.AddTail (strValue) End Sub Function tmGetList() As Object ' Данная функция передает ссылку на объект, ' содержащий список строк ' Этот пример вряд ли пригодится на практике, но он ' наглядно демонстрирует, как можно при вызове функции ' передать ей в качестве параметра объект, содержащий ' список строк. Set tmGetLlst = m_StringList End Function

Конечно, это - самое примитивное использование объекта VBList. Без сомнения, можно придумать что-то гораздо более полезное. Например, затратив минимум усилий преобразовать его в стек или в объект, имитирующий некоторую очередь. Я недавно использовал его как стек под программу настройки, созданную мною для Visual Basic.

Рассмотренный нами объект VBList - это лишь простой пример, как создать групповой объект автоматизации, с которым могут работать управляющие объекты механизма VBA. В статье были описаны скрытые от пользовател внутренние процессы обработки групповых объектов автоматизации, а также показано, как с помощью системы Delphi 2.0 получить удобную среду, позволяющую столь же легко, как и любой другой объект Delphi, создать объект Automation. Наконец, мы продемонстрировали, как с помощью собственного варианта интерфейса IEnumVariant настроить свои объекты Automation, чтобы в полной мере использовать преимущества, предоставляемые стандартным механизмом обработки групповых объектов Automation.


Литература:
Kraig Brockschmidt, Mastering OLE, Microsoft Press, 1995.
John Toohey, Using OLE 2.x in Application Development, Que Press, 1994.
Bryan Waters, Mastering OLE2, Sybex, 1995.