Главная » Delphi » Коллекции автоматизации

0

Прямо скажем:  нас, программистов, со всех сторон окружают программные объек ты, которые используются в качестве контейнеров для других программных объектов. Задумайтесь, как велико  их разнообразие — будь то массив,  список  (компонент TList), коллекция (компонент TCollection), класс контейнера шаблона  C++ или вектор Java. Кажется, что мы постоянно только  то и делаем, что подыскиваем оптимальный вариант пресловутой мышеловки для программных объектов, которая бы наилучшим образом справлялась с задачей хранения других программных объектов. Если оценить время, за траченное на создание идеального контейнерного класса,  то станет  ясно,  что это одна из самых важных  проблем, занимающих умы разработчиков. А почему бы и нет? Подоб ное  логическое разделение контейнера и его  содержимого помогает лучше организо вать алгоритмы и создает  вполне приемлемое соответствие реальному миру (в корзинке могут лежать  яйца,  в кармане — деньги, на стоянке можно  спокойно оставлять автомо били и т.д.). При изучении нового языка  или модели  разработки всегда приходится зна комиться с “новым способом” управления группами  некоторых элементов. Это и есть та самая  мысль,  к которой мы вас подводили: подобно любой  другой  модели  разработки программных продуктов, модель COM также имеет  свои способы управления собствен ными  разновидностями групп  элементов. Чтобы добиться эффективности  в разра ботке приложений COM, необходимо знать,  как обращаться с такими объектами.

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

type

IMyDisp = interface(IDispatch)

function GetProp(Index: Integer): Integer; safecall;

procedure SetProp(Index, Value: Integer); safecall;

property Prop[Index: Integer]: Integer read GetProp

write SetProp;

end;

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

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

•   Коллекции должны  содержать свойство _NewEnum, возвращающее интерфейс IUnknown объекту,  который поддерживает интерфейс IEnumVARIANT. Интер фейс  IEnumVARIANT будет использован для перечисления элементов в коллек ции.  Обратите внимание на то,  что  имя  этого  свойства должно  начинаться  с символа  подчеркивания, а само свойство — быть  отмечено в библиотеке типов как restricted (ограниченное). Диспетчерский идентификатор DispID для свой ства _NewEnum должен  быть  равен  значению DISPID_NEWENUM (-4), и опреде лен он будет в редакторе библиотеки типов Delphi  следующим образом:

function _NewEnum: IUnknown [propget, dispid $FFFFFFFC, restricted]; safecall;

•   Языки, поддерживающие конструкцию For..Each (например Visual Basic), бу дут использовать данный метод  для получения интерфейса IEnumVARIANT, не обходимого  для  перечисления элементов коллекции. (Более подробная ин формация по этой теме приведена в настоящей главе далее.)

•   Коллекции должны  обладать  методом Item(), который на основании индекса возвращает из коллекции элемент. Идентификатор диспетчера DispID для это го метода  должен  быть  равен  0, и его  следует  отметить флагом  стандартного элемента коллекции (default collection element). Если  бы понадобилось реализо вать  коллекцию указателей на интерфейсы IFoo, определение этого  метода  в редакторе библиотеки типов выглядело бы примерно так:

function Item(Index: Integer): IFoo [propget, dispid $00000000,defaultcollelem]; safecall;

Обратите внимание: параметр Index вполне может  иметь  тип  OleVariant, что  позволяет индексировать элементы с помощью значения  типа  Integer, WideString или любого другого типа.

•   Коллекции должны  обладать свойством Count, которое возвращает количество элементов в коллекции. Этот  метод  обычно определяется в редакторе библио теки типов следующим образом:

function Count: Integer [propget, dispid $00000001]; safecall;

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

•   Свойство или  метод,  возвращающие коллекцию, должны  иметь  имя,  представ ляющее  собой форму множественного числа,  образованного от имени  элементов коллекции. Например если  есть  свойство, возвращающее коллекцию элементов списка,  то в качестве имени  этого  свойства подошло бы слово  Items (если  эле мент коллекции имеет  имя Item). Аналогично элемент по имени  Foot (нога) бу дет входить  в свойство коллекцию по имени Feet (ноги). В тех редких  случаях, когда форма множественного числа для нужного слова совпадает с формой един ственного числа (например, для коллекции элементов fish или deer), имя свой ства коллекции должно  состоять из имени элемента, к которому присоединяется слово “Collection” (FishCollection или DeerCollection).

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

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

Реализация в Delphi

Тем, кому приходилось создавать элементы управления ActiveX в Delphi, вероятно известно, что в раскрывающемся списке  мастера ActiveX Control Wizard перечислено меньше  элементов управления, чем представлено в палитре компонентов IDE. Дело в том,  что  компания Borland не  допускает  отображения в списке  мастера некоторых элементов управления благодаря использованию функции RegisterNonActiveX(). Примером элемента управления, который доступен  в палитре компонентов, но не в окне  мастера, может  служить  элемент TListView, расположенный во вкладке  Win32 палитры. Причина “сокрытия” мастером элемента управления TListView состоит в том,  что  мастер  “не знает”,  что  ему делать  со свойством Items данного компонента, которое имеет  тип  TListItems. Поскольку мастер  не знает, какой  контейнер необ ходим для этого  типа свойства элемента управления ActiveX, то такой  элемент просто исключается из списка  мастера, чтобы  не создавать совершенно бесполезную оболоч ку для элемента управления ActiveX.

Однако  в случае с элементом TListView функция RegisterNonActiveX() вызы вается  с флагом  axrComponentOnly, который означает, что потомок компонента TListView будет помещен в список  мастера ActiveX Control Wizard. Предприняв не которые дополнительные меры  по созданию, казалось бы, бездействующего потомка компонента TListView по имени TListView2 и добавив  его в палитру, можно  полу чить  возможность создавать впоследствии элементы управления ActiveX, инкапсули рующие  элемент управления TListView. Потом, конечно, все равно  придется столк нуться  с той  же  самой  проблемой отказа  мастера создавать оболочки для  свойства Items и созданный элемент управления ActiveX придется признать бесполезным. К счастью, при  создании элемента управления ActiveX никто   не  принуждает ограни читься кодом,  созданным мастером, и в этом  случае можно  самостоятельно заняться свойством Items, чтобы  сделать  элемент управления полезным. Возможно, читатель уже начал  догадываться, что  идеальным способом для инкапсуляции свойства Items элемента TListView является коллекция.

Чтобы реализовать подобную  коллекцию элементов списка,  необходимо создать

новые  объекты, представляющие как отдельный элемент, так и всю коллекцию, а за тем добавить новое  свойство в стандартный интерфейс элемента управления ActiveX, который будет содержать данную  коллекцию. Начнем с определения объекта, пред ставляющего собой  элемент списка.  Присвоим ему имя  ListItem. Первым шагомразработки объекта ListItem будет создание нового объекта автоматизации с помо щью соответствующей пиктограммы во вкладке  ActiveX диалогового окна  New Items. После  создания этого  объекта заполним его свойства и методы  в редакторе библиоте ки  типов.  В целях  демонстрации добавим  в элемент TListView свойства Caption, Index, Checked и SubItems. Аналогичным образом создадим  второй новый объект автоматизации для самой  коллекции и присвоим ему имя ListItems. Пополним его описанными выше методами _NewEnum, Item(), Count(), Add() и Remove(). И, на конец, в стандартный интерфейс элемента управления ActiveX добавим  новое  свойст во по имени  Items, которое будет возвращать коллекцию.

После того как интерфейсы IListItem и IListItems будут полностью определе 

ны  в  редакторе  библиотеки  типов,  потребуется  вручную  внести  небольшие  измене

ния в файлы реализации, автоматически созданные для этих  объектов. Стандартным родительским классом  для нового объекта автоматизации является класс TAutoOb- ject; но новые  объекты будут создаваться только  внутренне (т.е.  без помощи фабри ки класса),  поэтому  изменим вручную тип предка  и укажем в качестве базового класс TAutoInfObject, который больше  подходит для объектов автоматизации создавае мых внутренне. Кроме того, поскольку  эти объекты не создаются с помощью  фабрики класса, из модулей можно удалить код инициализации, реализующий ненужные (в дан ном случае) фабрики.

Теперь, когда  вся  инфраструктура приняла надлежащий вид,  пора  приняться за реализацию объектов ListItem и ListItems. Объект ListItem —  самый  простой, поскольку   он  представляет собой  незатейливую оболочку   вокруг  элемента списка. Текст модуля, содержащего этот объект, представлен в листинге 15.11.

Листинг 15.11. Оболочка для элемента Listview

unit LVItem; interface uses

ComObj, ActiveX, ComCtrls, LVCtrl_TLB, StdVcl, AxCtrls;

type

TListItem = class(TAutoIntfObject, IListItem)

private

FListItem: ComCtrls.TListItem;

protected

function Get_Caption: WideString; safecall;

function Get_Index: Integer; safecall;

function Get_SubItems: IStrings; safecall;

procedure Set_Caption(const Value: WideString); safecall;

procedure Set_SubItems(const Value: IStrings); safecall;

function Get_Checked: WordBool; safecall;

procedure Set_Checked(Value: WordBool); safecall;

public

constructor Create(AOwner: ComCtrls.TListItem);

end;

implementation

uses ComServ;

constructor TListItem.Create(AOwner: ComCtrls.TListItem);

begin

inherited Create(ComServer.TypeLib, IListItem);

FListItem := AOwner;

end;

function TListItem.Get_Caption: WideString;

begin

Result := FListItem.Caption;

end;

function TListItem.Get_Index: Integer;

begin

Result := FListItem.Index;

end;

function TListItem.Get_SubItems: IStrings;

begin

GetOleStrings(FListItem.SubItems, Result);

end;

procedure TListItem.Set_Caption(const Value: WideString);

begin

FListItem.Caption := Value;

end;

procedure TListItem.Set_SubItems(const Value: IStrings);

begin

SetOleStrings(FListItem.SubItems, Value);

end;

function TListItem.Get_Checked: WordBool;

begin

Result := FListItem.Checked;

end;

procedure TListItem.Set_Checked(Value: WordBool);

begin

FListItem.Checked := Value;

end;

end.Заметьте: в конструктор передается объект ComCtrls.TListItem, который будет слу

жить в качестве элемента списка, контролируемого данным объектом автоматизации.

Реализация объекта коллекции ListItems сложнее, но не намного. Поскольку этот объект должен  обеспечивать объектную поддержку  типа  IEnumVARIANT, чтобы  реали зовать свойство _NewEnum, тип IEnumVARIANT обеспечен прямо в этом объекте. Следо вательно, класс  TListItems поддерживает оба  интерфейса: IListItems и  IEnum- VARIANT. Интерфейс IEnumVARIANT содержит четыре метода описанных в табл. 15.2.

Таблица 15.2. Методы интерфейса IEnumVARIANT

Метод                           Назначение

Next           Возвращает следующие n элементов коллекции

Skip           Пропускает n элементов коллекции

Reset          Переходит к первому элементу коллекции

Clone          Создает копию интерфейса IEnumVARIANT

Исходный  код  модуля,  содержащего  объект  ListItems,  представлен  в  листин

ге 15.12.

Листинг 15.12. Оболочка для элементов ListView

unit LVItems;

interface uses

ComObj, Windows, ActiveX, ComCtrls, LVCtrl_TLB, StdVcl;

type

TListItems = class(TAutoIntfObject, IListItems, IEnumVARIANT)

private

FListItems: ComCtrls.TListItems;

FEnumPos: Integer;

protected

{ Методы IListItems }

function Add: IListItem; safecall;

function Get_Count: Integer; safecall;

function Get_Item(Index: Integer): IListItem; safecall;

procedure Remove(Index: Integer); safecall;

function Get NewEnum: IUnknown; safecall;

{ Методы IEnumVariant }

function Next(celt: LongWord; var rgvar : OleVariant;

out pceltFetched: LongWord): HResult; stdcall;

function Skip(celt: LongWord): HResult; stdcall;

function Reset: HResult; stdcall;

function Clone(out Enum: IEnumVariant): HResult; stdcall;

public

constructor Create(AOwner: ComCtrls.TListItems);

end;

implementation

uses ComServ, LVItem;{ TListItems }

constructor TListItems.Create(AOwner: ComCtrls.TListItems);

begin

inherited Create(ComServer.TypeLib, IListItems);

FListItems := AOwner;

end;

{ TListItems.IListItems }

function TListItems.Add: IListItem;

begin

Result := LVItem.TListItem.Create(FListItems.Add);

end;

function TListItems.Get NewEnum: IUnknown;

begin

Result := Self;

end;

function TListItems.Get_Count: Integer;

begin

Result := FListItems.Count;

end;

function TListItems.Get_Item(Index: Integer): IListItem;

begin

Result := LVItem.TListItem.Create(FListItems[Index]);

end;

procedure TListItems.Remove(Index: Integer);

begin

FListItems.Delete(Index);

end;

{ TListItems.IEnumVariant }

function TListItems.Clone(out Enum: IEnumVariant): HResult;

begin

Enum := nil;

Result := S_OK;

try

Enum := TListItems.Create(FListItems);

except

Result := E_OUTOFMEMORY;

end;

end;

function TListItems.Next(celt: LongWord; var rgvar : OleVariant;

out pceltFetched: LongWord): HResult; stdcall;

var

V: OleVariant;

PV: PVariantArg;I: Integer;

begin

Result := S_FALSE;

try

if @pceltFetched <> nil then pceltFetched := 0;

PV := @rgvar;

for I := 0 to celt – 1 do begin

if FEnumPos >= FListItems.Count then Exit;

V := Get_Item(FEnumPos);

PV^ := TVariantArg(V);

// Прием, позволяющий защитить вариант от "сбора мусора".

// Поскольку вариант является частью массива elt, его нельзя

// ликвидировать.

TVarData(V).VType := varEmpty;

TVarData(V).VInteger := 0;

Inc(PV);

Inc(FEnumPos);

if @pceltFetched <> nil then Inc(pceltFetched);

end;

except

end;

if (@pceltFetched = nil) or ((@pceltFetched <> nil) and

(pceltFetched = celt)) then Result := S_OK;

end;

function TListItems.Reset: HResult;

begin

FEnumPos := 0;

Result := S_OK;

end;

function TListItems.Skip(celt: LongWord): HResult;

begin

Inc(FEnumPos, celt);

Result := S_OK;

end;

end.В этом  модуле единственным методом с нетривиальной реализацией является ме тод  Next(). Параметр celt метода  Next() указывает, сколько элементов должно быть  возвращено. Параметр elt содержит массив  типа  TVarArgs, содержащий по крайней мере  elt элементов. При  возврате параметр pceltFetched (если  не равен значению nil) должен  хранить реальное количество выбранных элементов. Этот  ме тод возвращает значение S_OK, если количество возвращаемых элементов совпадает с числом  затребованных; в противном случае возвращается значение S_FALSE. Логика данного метода  состоит в просмотре массива  elt и присвоении очередному его эле менту объекта типа  TVarArg, представляющего собой  элемент коллекции. Обратите внимание на маленький трюк,  который выполняется для очистки варианта типа Ole- Variant после  присваивания его  массиву.  Это  гарантирует, что  массив  не  подверг нется  автоматическому удалению  из памяти (“уборке  мусора”).  Если  бы это  не  былосделано, то содержимое массива  elt могло бы быть  удалено из памяти, при  заверше нии работы и освобождении объектов типа OleVariant, на которые ссылается пере менная V.

Подобно классу TListItem, конструктору класса  TListItems передается в каче стве параметра объект ComCtrls.TListItems, который участвует  в реализации раз личных методов этого класса.

И, наконец, для завершения реализации элемента управления ActiveX необходимо добавить механизм управления свойством Items. Прежде всего к объекту  следует до бавить поле для хранения коллекции:

type

TListViewX = class(TActiveXControl, IListViewX)

private

FItems: IListItems;

end;

Затем  в методе  InitializeControl() присваиваем переменной FItems новый экземпляр объекта класса TListItems:

FItems := LVItems.TListItems.Create(FDelphiControl.Items);

Наконец, метод  Get_Items() можно  реализовать с помощью простого возврата значения переменной FItems:

function TListViewX.Get_Items: IListItems;

begin

Result := FItems;

end;

Для проверки реальной работоспособности этой  коллекции можно  загрузить эле мент  управления  в  среде   Visual  Basic 6  и  попробовать  использовать  конструкцию For..Each для  работы с этой  коллекцией. На  рис. 15.16  показан результат запуска простого тестового приложения в VB.

Рис. 15.16. Приложение Visual Basic проверяет созданную коллекцию

На рис. 15.16 представлены две кнопки. Кнопка Command1 предназначена для до

бавления элементов в коллекцию listview, а кнопка  Command2 —  для опроса всехэлементов  коллекции  с  помощью  конструкции  For..Each и  добавления  восклица

тельных знаков  к каждому из них. Вот как выглядит реализация этих методов:

Private Sub Command1_Click() ListViewX1.Items.Add.Caption = "Delphi"

End Sub

Private Sub Command2_Click() Dim Item As ListItem

Set Items = ListViewX1.Items

For Each Item In Items

Item.Caption = Item.Caption + " Rules!!"

Next

End Sub

Несмотря на чувства, питаемые некоторыми программистами Delphi  к Visual Basic, мы должны  помнить, что  приложения Visual Basic — это  главный потребитель разра батываемых ими элементов управления ActiveX, и очень  важно  гарантировать надле жащее функционирование таких элементов управления в данной среде.

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

Источник: Тейксейра, Стив, Пачеко, Ксавье.   Borland Delphi 6. Руководство разработчика. : Пер.  с англ. — М. : Издательский дом “Вильямс”, 2002. —  1120 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии