Продолжается подписка на наши издания! Вы не забыли подписаться?

Создание COM-коллекций

Чистяков Владислав

Код к статье
пример коллекции на ATL/ascLib:
ftp://ftp.k-press.ru/pub/cs/2001/2/COMCollections/COMCollections.zip(~17 KB)
пример коллекции на ATL/STL (из MSDN)
ftp://ftp.k-press.ru/pub/cs/2001/2/COMCollections/atlcollections.zip(~22 KB)
Исходные коды ascLIb:
http://www.k-press.ru/Software/rus/ascLib/ascLib.asp(~260 KB)

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

Введение

Коллекция представляет собой эмуляцию динамического массива, заполненного данными того или иного типа. Использовать коллекцию очень просто.

Вот примеры использования коллекций на VB:

Dim coll As New CollectionName

coll.Add
coll.Add
coll.Add

MsgBox coll("Elem2").Number

coll.Add
coll.Add

MsgBox coll(3).Name

coll.Remove 2

coll.Remove "Elem4"

Dim s1 As String
s1 = "coll.Count = " & coll.Count & vbCrLf
Dim CollItem As CollectionItem
For Each CollItem In coll
s1 = s1 & CollItem.Name & vbCrLf
Next
MsgBox s1

Text1 = coll(2).Name
Text2 = coll("Elem5").Number

Как минимум, интерфейсы коллекции должны содержать свойства Count, Item и _NewEnum. Кроме этого, они могут предоставлять методы Add, Remove, Clear, Move и методы поиска.

Опишем вкратце каждый из них:

Свойство Item

IDL-описание:

[propget, id(0)] HRESULT Item([in] VARIANT Index, 
[out,retval] ICollectionItem ** CollectionItem);

VB-описание:

Item(index) As CollectionItem

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

Variant позволяет также осуществлять «умное» преобразование данных, например, если передана строка, но она не соответствует имени объекта, можно попытаться преобразовать строку в целое число и возвратить элемент с номером, соответствующим получившемуся числу. Другим примером может быть автоматическое преобразования индекса типа double в дату (если коллекция индексируется значениями типа Date). При получении указателя на IDispatch можно попытаться вызвать свойство, применяемое по умолчанию, и использовать в качестве индекса его значение, преобразовав тем самым значение IDispatch в строку или число. Правда, нужно такое ухищрение, скорее всего, чтобы избежать проблем, связанных с использованием Variant. Например, пользователь написал следующий код:

Coll(Text1) = 1233

то есть, вместо того, чтобы написать Text1.Text он указал просто Text1, законно предполагая, что VB вызовет default-свойство этого объекта. Но VB, видя, что параметр имеет тип Variant, просто-напросто подсунет указатель на dispatch-интерфейс объекта Text1. Чтобы не огорчать пользователя, коллекция должна сама обработать такую ситуацию.

Как уже говорилось, индекс может быть и другого (фиксированного) типа, например, целого или строкового. Представление коллекции в памяти не фиксировано и может быть любым, но чаще всего коллекции создаются с использованием массивов. Самые распространенные – ATL-реализации, простая или на STL массиве. Вне зависимости от того, какую из них использовать, придется изрядно потрудиться вручную, но STL-реализация предоставляет больше сервиса и является более производительной. Однако она не дает автоматической реализации именованной коллекции и имеет некоторые другие ограничения.

Свойство Item может называться и по-другому, но чаще всего применяется именно название Item. Это свойство может иметь любой DISPID, но чаще всего получает значение DISPID, равное DISPID_VALUE (0). Это позволяет в высокоуровневых языках типа VB опустить имя этого свойства:

MsgBox SomeObj.Item(1)
MsgBox SomeObj(1)

Вышеприведенные примеры равнозначны, так как свойство Item помечено как свойство по умолчанию.

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

Свойство _NewEnum

IDL-описание:

[propget, id(DISPID_NEWENUM)] _NewEnum([out, retval] IUnknown** ppvObject);

VB-описание:

В VB это свойство скрыто.

Это, пожалуй, главное свойство коллекции. Без него коллекция не может являться коллекцией. Оно используется VB в циклах «for each». С помощью этого свойства можно получить значение нескольких элементов коллекции (от одного до всех). Его имя и DISPID строго фиксированы. В VB оно помечается как скрытое. Чтобы коллекцию можно было использовать из VB, это свойство должно возвращать указатель на IEnumVARIANT. В принципе, можно реализовывать любой интерфейс перечислений, но VB поймет только IEnumVARIANT.

Свойство _NewEnum

IDL-описание:

[propget, id(0x68030000)] HRESULT Count([out, retval] long *nCount);

VB-описание:

Count As Long

Это свойство или метод, возвращающее количество элементов коллекции. Должно быть в любой коллекции, обычно имеет именно имя Count, не имеет фиксированного DISPID. Лучше создавать его как свойство.

Необязательные методы

Add – необязательный метод, позволяющий добавить новый элемент. Есть два варианта реализации этого метода. Первый подразумевает создание элемента коллекции внутри метода Add и возврат его через [out, retval]-параметр (возвращаемое значение в VB). Второй подразумевает добавление уже существующего элемента. Первый вариант выгоден, когда нужно избежать пользовательского контроля над созданием и уничтожением элемента коллекции, или когда элементы не имеют смысла вне коллекции. Второй – когда элементы необходимо создавать вне коллекции. Этот метод может вставлять новый элемент в конец коллекции, в подходящее место (в случае коллекции сортированных элементов) или в указанную позицию. В последнем случае позиция (индекс) передается в качестве параметра. В именованных коллекциях при добавлении нового элемента может происходить проверка имени элемента на уникальность или создание нового имени для добавляемого элемента.

Remove – тоже необязательный элемент, но если вы реализовали метод Add, то реализация метода Remove становится практически необходимостью. Этот метод может удалять последний элемент, но чаще всего он имеет параметр "индекс", соответствующий удаляемому элементу. Объект может иметь и другие методы, позволяющие добавить и удалить элементы, но они являются нестандартными и отдаются полностью на откуп программиста.

Find, FindItem, Search, Lookup – методы, позволяющие найти элемент коллекции. Они могут организовывать сложнейшие методы поиска, а могут делать то же, что и метод Item, но не генерируя ошибки.

Move – позволяет переместить элемент коллекции (изменить порядковый номер элемента). Редко встречается, но иногда бывает очень полезен. Реализация не фиксирована и целиком зависит на фантазию разработчика.

О самом интерфейсе коллекции нужно сказать, что он обязательно должен быть дуальным или disp-интерфейсом. Иначе некоторые языки будут воспринимать его некорректно. Желательно также пометить этот интерфейс как nonextensible. Это даст VB возможность сделать все необходимые ему проверки во время компиляции.

Итак, использование коллекций просто и удобно. Коллекции позволяют при внешней простоте и схожести с массивами осуществлять сложную логику. А что с созданием?

Реализация STL-коллекции

Для реализации интерфейсов коллекций в ваших объектах ATL предоставляет интерфейс ICollectionOnSTLImpl. Чтобы понять, как этот класс работает, создадим простой пример (ниже), использующий этот класс для реализации readonly-коллекции.

Чтобы сделать это, нужно:

Создание нового COM-объекта

Сначала создадим новый проект, и с помощью ATL COM AppWizard создадим Simple Object с названием Words. На закладке «Attributes» выберем «Support ISupportErrorInfo». Это необходимо для того, чтобы VB выводил осмысленные сообщения об ошибках, а не загадочное «Method XXX of object YYY failed…». Нажмем «OK». Убедимся, что сгенерирован dual-интерфейс IWords. Созданный класс будут использоваться для представления коллекции слов (то есть строк).

Изменение IDL-файла

Теперь откроем IDL-файл и добавим три свойства, необходимых, чтобы превратить IWords в интерфейс read-only коллекции:

   [
       object,
       uuid(0D44F689-B373-11D2-9A7F-50F653C10000),
       dual,                                             // (1)
       pointer_default(unique),
       nonextensible                                     // (2)
   ]
   interface IWords : IDispatch
   {
       [id(DISPID_NEWENUM), propget]                     // (3)
       HRESULT _NewEnum([out, retval] IUnknown** ppUnk);

       [id(DISPID_VALUE), propget]                       // (4)
       HRESULT Item(
                       [in] long Index, 
                       [out, retval] BSTR* pVal);        // (5)

       [id(0x00000001), propget]                         // (6)
       HRESULT Count([out, retval] long* pVal);
   };

Это стандартная форма интерфейса read-only коллекции, разработанной для VB-клиентов. Нумерованные комментарии в этом определении интерфейса соответствуют комментариям, приведенным ниже:

Создание Typedef-ов для типов данных коллекции

Теперь решим, как будут храниться данные, и как данные будут представлены в перечислении.

Для этого определим следующие typedef-ы:

// Хранить данные в массиве строк std::strings
// Так как для реализации коллекции выбран STL, то и для реализации массива
// лучше воспользоваться одним из шаблонов STL.
   typedef std::vector< std::string >         ContainerType;

// Интерфейс коллекции возвращает данные в виде BSTR.
   typedef BSTR                               CollectionExposedType;
   typedef IWords                             CollectionInterface;

   // Восспользуемся IEnumVARIANT в качестве перечислителя 
   // ( для совместимости с VB).
   typedef VARIANT                            EnumeratorExposedType;
   typedef IEnumVARIANT                       EnumeratorInterface;

В нашем случае, данные будут храниться как динамический массив (std::vector) строк (std::strings).

std::vector – это класс-контейнер библиотеки STL, представляющий динамический массив.

std::string – это строковый класс из Standard C++ Library. Эти классы упрощают работу с коллекциями строк.

Заметьте, что std::vector практически не содержит никаких ссылок на внешние библиотеки, и его смело можно использовать в своих приложениях. А вот std::string тащит за собой CRT, поэтому, использовав этот класс, вам придется удалить из опций release-версии проекта макрос _ATL_MIN_CRT. Иначе скомпилировать release-версию проекта не удастся.

Поскольку в большинстве случаев необходима поддержка Visual Basic, возвращаемый свойством _NewEnum перечислитель должен поддерживать интерфейс IEnumVARIANT.

Создание Typedef-ов для Copy Policy-классов

Теперь нужно создать typedef-ы для классов копирования, которые будут использоваться перечислителем и коллекцией:

    // Typedef для классов копирования
   typedef VCUE::GenericCopy<EnumeratorExposedType, ContainerType::value_type>    EnumeratorCopyType;
   typedef VCUE::GenericCopy<CollectionExposedType, ContainerType::value_type>    CollectionCopyType;

Можно применить для этого класс GenericCopy, определенный в VCUE_Copy.h and VCUE_CopyString.h, входящих в поставку ATL. Этот класс можно изменить для поддержки других типов данных.

Создание Typedef-ов для перечислений и коллекций

Теперь все параметры шаблона, нужные для определения классов CComEnumOnSTL и ICollectionOnSTLImpl имеются в виде typedef-ов. Чтобы упростить себе жизнь, создадим еще два typedef-а:

typedef CComEnumOnSTL< EnumeratorInterface, &__uuidof(EnumeratorInterface),
EnumeratorExposedType, EnumeratorCopyType, ContainerType > EnumeratorType;

typedef ICollectionOnSTLImpl< CollectionInterface, ContainerType,
CollectionExposedType, CollectionCopyType, EnumeratorType > CollectionType;

Теперь CollectionType – это синоним нашего варианта ICollectionOnSTLImpl, реализующего интерфейс IWords, определенный ранее, и предоставляющего перечислитель, поддерживающий IEnumVARIANT.

Редактирование кода, созданного визардом

Теперь нужно унаследовать CWords от реализации интерфейса, представленной CollectionType typedef, а не IWords:

class ATL_NO_VTABLE CWords : 
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CWords, &CLSID_Words>,
   public IDispatchImpl<CollectionType, &IID_IWords, &LIBID_ATLCOLLECTIONSLib>
{
public:
   DECLARE_REGISTRY_RESOURCEID(IDR_STRINGCOLLECTION)
   DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CWords)
   COM_INTERFACE_ENTRY(IWords)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IWords
public:
};

Добавление кода, заполняющего коллекцию

Осталось заполнить массив данными. Для нашего простого примера коллекции хватит и нескольких слов в конструкторе класса:

   CWords()
   {
       m_coll.push_back("this");
       m_coll.push_back("is");
       m_coll.push_back("a");
       m_coll.push_back("test");
   }

В результате у нас получилась коллекция строк индексируемая целым числом. У этой коллекции нет методов добавления/удаления элементов, и т.п. Чтобы реализовать именованную коллекцию со всеми необходимыми методами на STL, придется написать много своего кода. К тому же, если использовать некоторые из STL-шаблонов, проект может начать зависеть от CRT.

В качестве хорошей альтернативы созданию именованных на ATL/STL можно предложить использование ascLib, нашей собственной разработки. В этом журнале уже не раз упоминалась эта библиотека. В настоящее время она доступна для свободного использования как freeware. Единственная просьба авторов к ее пользователям – упоминание в ReadMe или About использования этой библиотеки или ее частей. Вся библиотека представлена в виде исходных кодов, архив которых можно скачать с нашего сайта, посетив страницу ascLib.asp. С помощью этой библиотеки создание именованных коллекций COM-объектов значительно упрощается. В ближайшее время планируется создание нескольких Визардов для VC6, которые позволят вообще избежать ручного кодирования.

Создание именованной коллекции на ascLib

Создадим коллекцию именованных объектов. Предположим, что наш объект, назовем его SomeCollection, кроме некоторых «полезных» методов будет содержать свойства Name и Number. Name будет доступно на запись и чтение, а Number – только на чтение. Соответственно, Name будет определять имя объекта, а Number – его порядковый номер в коллекции.

С помощью «ATL COM AppWizard» создадим новый проект и назовем его «Sample». Оставим все настройки визарда по умолчанию. В результате должен появиться проект COM DLL.

Создадим COM-объект, реализующий коллекцию. Для этого вызовем «ATL Object Wizard» и выберем «Simple Object». В поле «Short Name» зададим имя объекта – «SomeCollection». Как и при создании STL-коллекции, на закладке «Attributes» выберем «Support ISupportErrorInfo». Это необходимо для того, чтобы VB выводил осмысленные сообщения об ошибках, а не загадочное «Method XXX of object YYY failed…». Нажмем «OK».

Визард должен создать заголовочный и cpp-файл для нового класса, а также изменить IDL-файл, вставив туда пустое описание интерфейса ISomeCollection и CoClass-а CSomeCollection.

Теперь создадим COM-объект, который будет являться элементом коллекции. Выполним те же действия, что и для объекта SomeCollection, но в качестве имени зададим «CollectionItem».

Подключение ascLib

Теперь необходимо подключить к проекту ascLib. ascLib не имеет в своем составе библиотек кроме одной отладочной DLL. Поэтому подключим заголовочные файлы с помощью директивы #include. Для начала необходимо поместить путь к ascLib в путь включаемых файлов (Tools\Options->Directories->Show directories for:Include files - см. рисунок ниже).

Откроем с помощью закладки «FileView» файл stdafx.h и изменим его так, чтобы он выглядел следующим образом:

// stdafx.h : include file for standard system include files,
//      or project specific include files that are used frequently,
//      but are changed infrequently

#if !defined(AFX_STDAFX_H__5557C757_2FF7_46B3_B31A_37FC04A89D40__INCLUDED_)
#define AFX_STDAFX_H__5557C757_2FF7_46B3_B31A_37FC04A89D40__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define STRICT
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define _ATL_APARTMENT_THREADED

#include <atlbase.h>
//You may derive a class from CComModule and use it if you want to override
//something, but do not change the name of _Module
// >>
#include "ascLibInit.h"
// <<
extern CComModule _Module;
#include <atlcom.h>

// >>
#include "ascLib.h"
#include "ascArray.h"
#include "ascCollectionImpl.h"
// <<

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_STDAFX_H__5557C757_2FF7_46B3_B31A_37FC04A89D40__INCLUDED)

Вставленные фрагменты отмечены жирным и заключены в комментарии "// >>" и "// <<". Учтите, порядок вставки очень важен!

Теперь в конец файла stdafx.cpp вставте строчку:

#include "ascLib.cpp"
С некоторого момента ascLib обзавелась общей отладочной DLL-библиотекой. Она нужна только на стадии отладки. При запуске DEBUG-версии проекта эта библиотека должна находиться или в путях компьютера или в том же каталоге, что и отлаживаемая программа. Если вам доставляет неудобство ее использование, то просто объявите директивой #define (или в переменной debug-версии проекта) следующую константу _NO_ASCDEBUGBREAKONFAILURE. Это отключит расширенные отладочные функции ascLib и упростит вам жизнь. Если вы этого не сделали, необходимо добавить ascLib в список библиотек в настройках проекта на закладке Link (см. рисунок ниже).

Вот и все. Теперь можно использовать ascLib.

Изменение IDL-описания

Для начала нам нужно изменить IDL-описание, добавив в в интерфейс ICollectionItem свойства Name и Number. Проще всего это сделать с помощью средств VC. Для этого необходимо открыть контекстное меню интерфейса ICollectionItem (на закладке ClassView) и выбрать пункт «Add Property...» (см. рисунок ниже).

В конце у вас должно получиться примерно такое IDL-описание:

[
   object,
   uuid(4F363BF5-27A6-4B03-B808-29B0A96A5592),
   dual,
   helpstring("ICollectionItem Interface"),
   pointer_default(unique),
   nonextensible
]
interface ICollectionItem : IDispatch
{
   [propget, id(1)]  HRESULT Name([out, retval] BSTR *pVal);
   [propput, id(1)]  HRESULT Name([in] BSTR newVal);
   [propget, id(2)]  HRESULT Number([out, retval] long *pVal);
};

Интерфейс ISomeCollection лучше отредактировать вручную, так как реализацию его методов вам делать не придется. Его реализация уже есть в шаблонах ascLib. Но если вам хочется изменить начальную функциональность любого метода, то вы можете определить в классе CSomeCollection любой из них и наполнить его своей функциональностью. Естественно, вы всегда можете вызвать метод базового класса. Если же вы не внесете метод ни в описание класса, ни в IDL-описание, то компилятор вообще не включит его реализацию в исполняемый файл.

Вы можете описать в IDL следующие методы:

// Константы для указания DISPID
typedef [v1_enum] enum
{
//	DISPID_UNKNOWN = -1,
//	DISPID_VALUE = 0,
//	DISPID_NEWENUM = -4,
	DISPID_CollectionNewEnum = DISPID_NEWENUM,
	DISPID_CollectionItem = DISPID_VALUE,
	DISPID_CollectionCount = 0x68030000,
	DISPID_CollectionAdd = 0x60030001,
	DISPID_CollectionRemove = 0x60030002,
	DISPID_CollectionFindItem = 0x60030003,
	DISPID_CollectionMove = 0x60030004,
} CollectionDISPIDs;

[propget, id(DISPID_CollectionItem), 
helpstring("Возвращает объект XXX по имени или индексу.")]
HRESULT Item([in] VARIANT Index, [out,retval] IXXX ** XXX);

// Restricted, ReadOnly свойство _NewEnum необходимо для перебора в VB.
[propget, id(DISPID_CollectionNewEnum), restricted, helpstring("")]
HRESULT _NewEnum([out, retval] IUnknown** ppvObject);

// ReadOnly свойство Count - позволяет получить количество объектов 
// в коллекции.
[propget, id(DISPID_CollectionCount), 
helpstring("Количество объектов в коллекции.")]
HRESULT Count([out, retval] long *nCount);

[id(DISPID_CollectionAdd), helpstring("Добавляет новый элемент.")] 
HRESULT Add([in, defaultvalue(-1)] long Index, [out, retval] IXXX ** pElem);

[id(DISPID_CollectionRemove), 
helpstring("Позволяет удалить элемент по имени или индексу")] 
HRESULT Remove([in] VARIANT Index);

[propget, id(DISPID_CollectionFindItem), 
helpstring("Позволяет найти элемент коллекции по имени или индексу.")] 
HRESULT FindItem([in] VARIANT Index, [out,retval] IXXX ** ppVal);

[id(DISPID_CollectionMove), 
helpstring("Позволяет переместить элемент коллекции на другое место.")] 
HRESULT Move([in] VARIANT Index, [in] long NewIndex);

Вместо XXX нужно подставить имя интерфейса нашего элемента коллекции, то есть ICollectionItem. Если вам не нужен какой-то метод, просто не вносите его IDL-описание. Если нужен метод с описанием, отличным от приведенного, то добавьте свой и воспользуйтесь имеющейся функциональностью для реализации его работы.

Изменение класса CCollectionItem

В начале, перед определением класса CCollectionItem (в файле CollectionItem.h) необходимо поместить предекларацию класса, реализующего коллекцию – CSomeCollection:

// Предекларация класса, реализующего коллекцию
class CSomeCollection;

Теперь изменим описание класса CCollectionItem и унаследуем его от шаблона CascObjectCollectionElement, как это показано ниже:

class ATL_NO_VTABLE CCollectionItem : 
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CCollectionItem, &CLSID_CollectionItem>,
public ISupportErrorInfo,
public CascObjectCollectionElement<CSomeCollection>,
public IDispatchImpl<ICollectionItem, &IID_ICollectionItem, &LIBID_SampleLib>
{

Теперь создадим реализации методов доступа к свойствам. Для этого переместимся в конец файла CollectionItem.h и в конце декларации класса добавим необходимый код:

// ICollectionItem
public:
   STDMETHOD(get_Name)(/*[out, retval]*/ BSTR *pVal);
   STDMETHOD(put_Name)(/*[in]*/ BSTR newVal);
   STDMETHOD(get_Number)(/*[out, retval]*/ long *pVal);
// CascObjectCollectionElement
public:
   BSTR ascObjectName(){ return m_sbsName.Copy(); }
   CComBSTR m_sbsName;
};

Реализацию самих методов переместим в С++-файл. Почему Wizard ленится это делать, совершенно непонятно.

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

Метод ascObjectName() будет использоваться классом CSomeCollection. Он должен возвращать значение переменной m_sbsName (как это показано выше).

Теперь пришла очередь реализовать методы доступа к свойствам:

Методы get_Name и get_Number можно реализовать в заголовочном файле, однако метод put_Name можно реализовать только в cpp-файле. Это связано с тем, что в этом методе происходит доступ к массиву, находящемуся в классе CSomeCollection. Вставить #include "SomeCollection.h" нельзя, так как получится циклическая ссылка. Чтобы соблюсти единообразие, мы поместили реализации всех трех методов в cpp-файл (CollectionItem.cpp). Ниже приведена реализация этих методов:

// Описание класса CSomeCollection необходимо для реализации put_Name
#include "SomeCollection.h"

// Метод put_Name должен быть реализован в C++-файле так как его 
// реализация нуждается в описании класса CSomeCollection.
STDMETHODIMP CCollectionItem::put_Name(/*[in]*/ BSTR newVal)
{
   CComPtr<ICollectionItem> spICollectionItem;
   CComVariant svStrIndex(newVal);
   // m_pCollectionOwner реализуется template'ом CascObjectCollectionElement
   // и позволяет получить доступ к реализации самой коллекции.
   if(m_pCollectionOwner)
   {
      HRESULT hr = m_pCollectionOwner->get_FindItem(svStrIndex, &spICollectionItem);
      if(hr == S_OK)
      {
         ASC_CNV; // ASC_CNV - это замена трудному в написании SUCCEEDED ;O)
         // А макрос ASC_RET_ERR_1 облегчает закладку сообщения об ошибке.
         // Само сообщение должно быть описано в ресурсном файле и иметь имя
         // IDS_E_ASC_INVALID_COLLECTION_NAME. Также необходимо описать и константу.
         // Но все это надо делать только если вы создаете собственное собщение.
         // А в текущем случае можно воспользоваться строками и константами из ascLib.
         ASC_RET_ERR_1(E_ASC_INVALID_COLLECTION_NAME, 
            &__uuidof(ICollectionItem), OLE2T(newVal)); 
      }
  }

   m_sbsName = newVal;
   return S_OK;
}

STDMETHODIMP CCollectionItem::get_Name(/*[out, retval]*/ BSTR *pVal)
{
   // ascLib содержит несколько макросов, упрощающих обработку ошибок.
   // Полный их список можно увидеть в ascLib.h. Кроме улучшения читаемости
   // программы, эти макросы содержат некоторые отладочные функции.
   ASC_RET_E_POINTER(pVal);
   *pVal = m_sbsName.Copy();
   return S_OK;
}

STDMETHODIMP CCollectionItem::get_Number(/*[out, retval]*/ long *pVal)
{
   // Проверяет указатель и если он NULL выходит из функции возвращая E_POINTER.
   ASC_RET_E_POINTER(pVal); 
   *pVal = m_nNumber;
   return S_OK;
}

Метод put_Name реализован следующим образом. Сначала проверяется, есть ли в коллекции элемент с переданным индексом. Если индекс найден, возбуждается ошибка. Это делается с помощью макроса ASC_RET_ERR_1. Этот макрос вынимает строку из ресурса (в данном случае это стандартная строка, сообщающая о неправильном использовании индекса) и формирует строку сообщения об ошибке, помещая ее в качестве описания COM-ошибки. После закладки сообщения происходит выход из метода с возвратом ошибки. Если элемент с переданным именем не найден, то имя присваивается в переменную m_sbsName. Таким образом достигается уникальность имен в рамках коллекции.

Изменение класса CSomeCollection

Теперь настала пора модифицировать класс CSomeCollection. Перед его описанием необходимо поместить:

#include "CollectionItem.h"

Это нужно потому, что методы базовых классов осуществляют доступ к внутренностям элементов коллекции. В самом классе нам необходимо сделать всего два действия, первое – унаследовать его от шаблона CascFullComCollectionOfNamedComObjectImpl, второе – реализовать метод GetCollectionElemBaseName, который должен возвращать необходимый префикс для имени нового элемента коллекции.

Первое действие можно упростить, если воспользоваться макросом:

ASC_COM_COLLECTION_OF_NAMED_COM_OBJECT_IMPL(SomeCollection, ItemName)
Дело в том, что шаблон CascFullComCollectionOfNamedComObjectImpl требует, чтобы ему передали довольно много параметров:

TCollectionClass имя класса, реализующего коллекцию
TCollectionItemClass имя класса, реализующего элемент коллекции
TICollection имя интерфейса коллекции (интерфейс должен содержать некоторые из приведенных выше IDL-описаний)
TICollectionItem интерфейс элемента коллекции
ArrayType (необязательный параметр) тип массива, хранящего список элементов
nSkippedElemCount (необязательный параметр) количество пропускаемых элементов
plibid (необязательный параметр) указатель на идентификатор библиотеки типов (LibID) необходимо задавать, если описание интерфейсов находится в другой библиотеке типов.

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

Имя интерфейса = имя COM-объекта плюс префикс «I».

Имя C++ класса = имя COM-объекта плюс префикс «C».

Макрос ASC_COM_COLLECTION_OF_NAMED_COM_OBJECT_IMPL использует эти умолчания, чтобы упростить работу. Вот тело этого макроса:

#define ASC_COM_COLLECTION_OF_NAMED_COM_OBJECT_IMPL(SomeCollection, ItemName) \
public CascFullComCollectionOfNamedComObjectImpl \
<C##SomeCollection, C##ItemName, \
I##SomeCollection, I##ItemName>

Как видите, макрос использует соглашения ATL об именовании классов и интерфейсов. Так что вам остается задать имя CoClass-ов коллекции и элемента коллекции соответственно.

Вот измененная часть кода класса CSomeCollection:

class ATL_NO_VTABLE CSomeCollection : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CSomeCollection, &CLSID_SomeCollection>,
  public ISupportErrorInfo,
  // Не забудьте закомментировать следующую строчку:
  //public IDispatchImpl<ISomeCollection, &IID_ISomeCollection, &LIBID_SampleLib>
  ASC_COM_COLLECTION_OF_NAMED_COM_OBJECT_IMPL(SomeCollection, CollectionItem)
{
public:
  LPTSTR GetCollectionElemBaseName()
  {
    return _T("Elem");
  }

Метод GetCollectionElemBaseName возвращает префикс, используемый для генерации имени при добавлении, методом Add, нового элемента коллекции.

Ресурсы

Реализация коллекций в ascLib подразумевает, что при возникновении ошибки (наподобие неправильного индекса) будет возвращен ее код и заложено ее текстовое описание (коды стандартных для ascLib ошибок можно найти в ascErrors.h). Соответствующие им строковые ресурсы разложены на несколько файлов (для экономии места в исполняемых модулях). Ресурсы, необходимые для работы коллекций, лежат в файлах ascCollectionsErrorStrings.rc и ascCommonErrorStrings.rc

Это очень небольшие файлы, которые проще всего включить в свой rc-файл (Sample.rc) при помощи директивы #include.

Для этого необходимо открыть файл ресурсов проекта в текстовом виде. Это можно сделать или с помощью диалога открытия файла, где надо жестко задать в поле «Open As» значение «Text», или воспользоваться любым текстовым редактором, скажем, Notepade. Открыть файл ресурсов в текстовом виде из среды можно только с помощью макросов или утилит независимых поставщиков.

Открыв Sample.rc, нужно найти в нем секцию «TEXTINCLUDE DISCARDABLE», содержащую строку «"#include ""winres.h""\r\n"» и отредактировать эту секцию, чтобы она выглядела следующим образом:

2 TEXTINCLUDE DISCARDABLE 
BEGIN
  "#include ""winres.h""\r\n"
  "#include ""ascCollectionsErrorStrings.rc""\r\n"
  "#include ""ascCommonErrorStrings.rc""\r\n"
  "\0"
END

Теперь необходимо изменить файл resource.h. Внесите в его начало следующие строки:

// >>
#include "ascErrorStrings.h"
// <<

Естественно, что директорию, где находится ascLib, перед этим необходимо поместить в путь включаемых файлов.

Если неохота возиться с ресурсами, можно перехватить процедуры обработки ошибок и реализовать их по-своему.

Тестирование

Вот, собственно, и все. Мы создали полнофункциональную коллекцию с именованными элементами, которые можно индексировать как по имени, так и по порядковому номеру. Причем имя можно читать и менять непосредственно у элемента коллекции.

Осталось написать небольшой тест, чтобы проверить ее функциональность. Скорее всего, вы и сами легко справитесь с этой задачей. Если же вам лень это делать самостоятельно, код такого тестового примера можно получить с нашего сайта:

ftp://ftp.k-press.ru/pub/cs/2001/2/COMCollections/COMCollections.zip.

Этот zip содержит проект (Sample) реализующий объект-коллекцию и тестовый VB-проект (SampleTest).


Copyright © 1994-2016 ООО "К-Пресс"