Введение в OLE–автоматизацию

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

Итак, разработчик может сделать приложение программируемым, определив объекты (так называемые, объекты автоматизации, Automation objects) и интерфейсы COM, методы которых будут прямо отображаться на внутренние функции приложения. Обычно, хотя и не обязательно, для этих целей используется стандартный интерфейс IDispatch, разработанный группой Microsoft Visual Basic. Необходимость в этом интерфейсе возникла на заре OLE-автоматизации из-за того, что Visual Basic, являясь одним из наиболее распространенных средств создания сценариев для программируемых приложений, не поддерживал возможности вызова методов обычных COM-интерфейсов с виртуальной таблицей. По традиции, с которой приходится мириться программистам на других языках, OLE-автоматизация большинства приложений и сегодня реализуется с помощью IDispatch. Этот интерфейс в настоящее время поддерживается Microsoft Word, Microsoft Excel и массой других приложений.

Интерфейс IDispatch спроектирован таким образом, что клиент с его помощью может обращаться к произвольной группе методов, передавая любые необходимые параметры. Чтобы это действовало, разработчик объекта, реализующего IDispatch, должен определить, какие в точности методы будут доступны. Это достигается определением диспетчерского интерфейса (dispatch interface), сокращенно – диспинтерфейса (dispinterface). Каждый экземпляр стандартного интерфейса IDispatch (объект может поддерживать несколько экземпляров одновременно) обеспечивает доступ к определенному диспинтерфейсу.

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

Важнейшей задачей клиента, использующего IDispatch, является маршалинг параметров запросов. Напомним, что маршалинг (упаковка параметров для пересылки между процессами) обычного COM-интерфейса с виртуальной таблицей выполняется заместителем и заглушкой (proxy, stub). В данном же случае, клиент сам обязан выполнить упаковку параметров для метода диспинтерфейса в некую стандартную форму, называемую вариантом (variant), а также – распаковку из варианта результатов вызова, возвращенных методом. Вариант определяет стандартную форму представления каждого параметра и идентификатор типа параметра для всех типов, используемых Visual Basic: короткое целое, длинное целое, строка символов и т.д. Программисты на других языках поэтому должны использовать лишь известные Visual Basic типы.

Позднее связывание через диспинтерфейс клиента на Delphi

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

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

uses ComObj;

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

Следующий пример иллюстрирует вызовы метода автоматизации. Фрагмент запустит программу Microsoft Word и сохранит в файле документ из двух строк. Функция CreateOleObject (определенная в ComObj) возвращает ссылку на IDispatch объекта автоматизации, совместимую по присваиванию с вариантным типом.

var

  Word: Variant;

begin

  Word := CreateOleObject('Word.Basic');

  Word.FileNew('Normal');

  Word.Insert('This is the first line'#13);

  Word.Insert('This is the second line'#13);

  Word.FileSaveAs('c:\temp\test.txt', 3);

end;

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

Некоторые серверы автоматизации позволяют опускать параметры вызова метода, принимая их значения по умолчанию. Например,

Word.FileSaveAs('test.doc');

Word.FileSaveAs('test.doc', 6);

Word.FileSaveAs('test.doc',,,'secret');

Word.FileSaveAs('test.doc', Password := 'secret');

Word.FileSaveAs(Password := 'secret', Name := 'test.doc');

Параметры методов автоматизации могут быть следующих типов: integer, real, string, Boolean и variant. Параметр передается по ссылке, если выражение параметра состоит только из ссылки на переменную типа Byte, Smallint, Integer, Single, Double, Currency, TDateTime, AnsiString, WordBool или Variant. Если выражение имеет какой-либо другой тип, или если оно не является только переменной, параметр передается значением. Передача параметра по ссылке методу, ожидающему параметр-значение, заставляет COM выбирать значение из параметра-ссылки. Наоборот, передача параметра-значения методу, который ожидает параметр-ссылку, вызывает ошибку.

Таким образом, методам автоматизации можно передавать типизированные параметры. При этом процедура маршалинга скрыта от программиста. Однако самым эффективным методом передачи является обмен между клиентом и сервером двоичными данными, описываемыми как массивы элементов типа varByte. Для таких массивов не производится какой-либо скрытой обработки в целях маршалинга, и можно эффективно обращаться к ним, используя подпрограммы VarArrayLock и VarArrayUnlock.

Рассматривая приведенные примеры, можно задать вопрос, каким образом имена объектов (например, Word.Basic) или методов (в данном случае, FileNew, Insert и FileSaveAs) транслируются в уникальные идентификаторы? Если не будет обеспечен данный механизм, невозможным станет связывание в режиме выполнения. Прежде всего, рассмотрим наш пример так называемого программного идентификатора (programmatic identifier или сокращенно ProgID) «Word.Basic». Программный идентификатор – это удобный синоним уникального идентификатора класса CLSID. Напомним, что в модели COM класс объекта идентифицирует определенную реализацию объекта  или группы интерфейсов. Отображение удобочитаемого ProgID в CLSID содержится в реестре – оно помещается туда при установке программы Word на компьютер. Таким образом, любой клиент, вызвав CreateOleObject с ProgID в качестве параметра, запустит соответствующий сервер автоматизации.

Что касается идентификаторов методов и свойств, то соответствующая информация связывания может быть получена клиентом из библиотеки типа или же путем обращения непосредственно к объекту через IDispatch. Например, метод IDispatch::GetIDsOfNames специально предназначен для этих целей: он возвращает идентификатор метода или свойства диспинтерфейса в ответ на запрашиваемое клиентом имя. Данный второй способ в свое время был разработан группой Visual Basic и является наиболее простым, хотя и не самым эффективным. Именно он используется встроенным типом Variant Delphi для производства позднего связывания.

Еще один важный вопрос: где программист может получить сведения о реализуемых сервером объектах автоматизации? В данном случае наиболее вероятный источник информации для рассматриваемого примера – интерактивная справочная система Microsoft Word с полным списком объектов в этом приложении, которые можно использовать для автоматизации, их методов и свойств и вообще всего, что необходимо для обращения к их сервисам. В общем случае приложения, предоставляющие сервисы через автоматизацию, содержат весьма подробную документацию на сей счет.

Учебный пример клиента Microsoft Word

Одной из необходимых функций большинства приложений, работающих с данными, является генерация различного рода отчетов. Как известно, для реализации генератора отчетов по выборкам из баз данных программист на Delphi имеет в своем распоряжении штатные средства: компоненты из вкладок «QReport»  и «Decision Cube» палитры компонентов. Разработанный таким образом генератор становится частью приложения, встроен в него, сопровождается специализированным интерфейсом, реализованным как часть интерфейса всего приложения. Часто, однако, пользователям необходимы отчеты в форме, пригодной для некоторой дальнейшей обработки. Удобно для этих целей использовать общедоступные офисные приложения (Word, Excel и т.п.). В этом случае задачей генератора отчетов становится выдача файлов в соответствующих форматах (*.doc, *.xls и т.п.). Можно пойти дальше: реализовать распределенное приложение, в котором генератор отчетов будет клиентом СУБД с одной стороны и клиентом сервера автоматизации (Word, Excel и т.п.) с другой.

Рассмотрим простой пример такого приложения, генерирующего отчет при помощи Microsoft Word. Данные для отчета будут выбираться из демонстрационной базы данных с псевдонимом DBDEMOS, входящей в поставку Delphi. Для доступа к какой-либо из таблиц этой базы данных необходимо на форму разрабатываемого приложения поместить компонент TTable, установив соответственно его свойства DatabaseName и TableName. Алгоритм чтения данных из таблицы может быть, например, следующим:

  Table1.Open;

  with Table1 do while not Eof do begin

// прочитать значение полей из Fields.Fields[i].AsString;

      Next

  end;

  Table1.Close;

Для корректного соединения с OLE-сервером клиент должен, прежде всего, сделать попытку обнаружить уже запущенный сервер. И только в случае неудачи – создать новый объект автоматизации. Таким образом, пример из предыдущего раздела целесообразно доработать, как показано ниже:

  try // если Word запущен - подключиться к нему

    W := GetActiveOleObject('Word.Application');

  except    // если нет - запустить

    W := CreateOleObject('Word.Application');

  end;

Помимо прочего, в данном примере мы изменили программный идентификатор, использовав вместо «Word.Basic» более современный объект автоматизации «Word.Application». К сведению, объект «Word.Basic» инкапсулирует конструкции одноименного языка, обеспечивавшего программируемость прежних версий приложения: Word version 6.0 и Word for Windows 95. Его реализация сохранена для обеспечения совместимости современных версий Word со старыми клиентами.

Ниже приведен полный код генератора отчета, который реализован как отклик на событие нажатия кнопки:

procedure TForm1.Button1Click(Sender: TObject);

var   W,D,S,PosBeg,PosEnd: Variant; i,j: Integer; ws: WideString;

begin

  with Table1 do begin

      DatabaseName:='DBDEMOS'; TableName:='country.db'; Open

  end;

  try // если Word запущен - подключиться к нему

      W := GetActiveOleObject('Word.Application');

  except    // если нет - запустить

      W := CreateOleObject('Word.Application');

  end;

  W.Visible := True;

  D:=W.Documents.Add;

  S:=W.Selection;

  S.TypeText('Hello, World!'#13);

  PosBeg := S.Start;

  j:=0; ws:='№';

  for i:=0 to Table1.FieldCount-1 do

      ws:=ws+#9+Table1.Fields.Fields[i].FieldName;

  ws:=ws+#13;

  S.TypeText(ws);

  with Table1 do while not Eof do begin

      j:=j+1;

      ws:=IntToStr(j)+'.';

      for i:=0 to FieldCount-1 do

        ws:=ws+#9+Fields.Fields[i].AsString;

      ws:=ws+#13;

      S.TypeText(ws);

      Next

  end;

  PosEnd := S.Start;

  Table1.Close;

  D.Range(PosBeg,PosEnd).ConvertToTable(Separator:=#9,

      AutoFit:=True,AutoFitBehavior:=1,DefaultTableBehavior:=1)

end;

Пожалуйста, не забудьте вставить ссылку на упомянутый модуль ComObj. Напомним также о необходимости изучения документации по программированию Microsoft Word при разработке его клиентов автоматизации. В данном пособии мы имеем возможность лишь следующего краткого пояснения. Для преобразования введенных текстовых данных в таблицу использован простой прием: текущая позиция документа запоминается дважды, до и после ввода преобразуемого текста:

  PosBeg := S.Start;    // начальная позиция

  S.TypeText(ws); ...   // вывод текста

  PosEnd := S.Start;    // конечная позиция

После чего весь отмеченный диапазон преобразуется в таблицу вызовом метода ConvertToTable. Заметим, что в данном случае использованы именованные параметры. Обратите внимание также на использованный в программе тип WideString. Именно его целесообразно использовать для представления текстовой информации при программировании OLE-автоматизации, поскольку этот тип совместим со стандартным COM типом представления строк BSTR. Приложение будет более эффективным, если вместо конвертирования строк из внутренних типов Delphi в WideString каждый раз для передачи серверу, с самого начала сохранять текст в требуемом формате.

Результат работы приложения – содержимое таблицы country.db демонстрационной базы данных DBDEMOS, помещенное в виде отчета в документ Microsoft Word – представлен на рисунке ниже:

Рис. 1.  Отчет в документе Microsoft Word

Особенности программирования на C++Builder

Разработка клиента, взаимодействующего с OLE-сервером через диспинтерфейс, возможна также в среде программирования Borland C++Builder, хотя и требует написания несколько более обширного кода. Дело в том, что разработчики C++Builder для реализации такой возможности решили обойтись средствами, предоставляемыми стандартным языком C++, в то время как разработчики Delphi пошли по пути расширения стандарта с целью упрощения синтаксиса.

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

#include <utilcls.h>

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

·        Метод Variant::Exec предназначен для запуска методов диспинтерфейсов, а также – для считывания и установки их свойств.

·        Шаблоны методов класса Variant, названные OleFunction, OleProcedure, OlePropertyGet и OlePropertySet, реализуют более удобный способ доступа к методам и свойствам диспинтерфейса в смысле удобочитаемости текста программы.

Например, команда установки свойства диспинтерфейса через объект W класса Variant, взятая из предыдущего примера на Delphi:

  W.Visible := True;

на языке C++ может быть реализована двумя способами:

      W.Exec(PropertySet("Visible")<<true);

или

      W.OlePropertySet("Visible",true);

В данном случае второй способ более нагляден, поскольку требует указания лишь двух параметров, в том числе – имени свойства. Первый способ реализован более замысловато: методу Exec передается только один параметр – объект класса PropertySet, специально разработанного для совместной работы с Exec. Его описание вы можете найти в заголовке sysvari.h. Там можно в частности обнаружить, что базовым классом для PropertySet является класс AutoCmd:

  class AutoCmd

  {

  public:

      ...

    // By value args

    AutoCmd& operator <<(const Variant& arg);

    AutoCmd& operator <<(const short arg);

    AutoCmd& operator <<(const int arg);

    AutoCmd& operator <<(const float arg);

      ...

  };

  class PropertySet : public AutoCmd

  {   ...

  };

Для базового класса AutoCmd перегружена операция <<, чтобы реализовать удобный способ передачи параметров методам диспинтерфейса. Эта операция описана для всех типов, используемых встроенным механизмом маршалинга, который, как и в случае Delphi, скрыт от программиста (выше приведен лишь небольшой фрагмент описания, содержащий только четыре типа). Другими наследниками AutoCmd являются классы PropertyGet, Procedure и Function, предназначенные для аналогичного использование в качестве параметра Exec. PropertyGet – для считывания значения свойства, Procedure и Function – для вызова методов диспинтерфейса. Например, перевод фрагмента рассматриваемого примера на Object Pascal Delphi:

  S.TypeText('Hello, World!'#13);

  PosBeg := S.Start;

выглядит на С++ следующим образом:

      S.Exec(Procedure("TypeText")<<WideString("Hello, World!\n"));

      PosBeg = S.Exec(PropertyGet("Start"));

Здесь для надежности использовано явное преобразование к рассмотренному ранее типу WideString. Другой способ «перевода», с использованием шаблонов методов Variant, выглядит так:

      S.OleProcedure("TypeText",WideString("Hello, World!\n"));

      PosBeg=S.OlePropertyGet("Start");

Поясним работу данного описания. Методы OleProcedure, OleFunction, OlePropertyGet и OlePropertySet класса Variant описаны с использованием механизма шаблонов. Это дает возможность использовать при их вызовах параметры произвольных типов. Более того, эти шаблоны перегружены, что позволяет передавать произвольное количество параметров. Можно убедиться, рассматривая соответствующий заголовочный файл, что допустимо использовать вплоть до десяти параметров (помимо основного, обязательного параметра, означающего имя метода диспинтерфейса). В нашем примере на Delphi мы задавали диапазон для преобразование его в таблицу:

  D.Range(PosBeg,PosEnd)

Два способа написания этого на C++:

      D.OleFunction("Range",PosBeg,PosEnd)     D.Exec(Function("Range")<<PosBeg<<PosEnd)

Как видно, использование шаблонов методов Variant в большинстве случаев более наглядно. Однако метод Exec незаменим, если требуется передать именованные параметры. В нашем примере вызов метода ConvertToTable использует именованные параметры:

ConvertToTable(Separator:=#9,

  AutoFit:=True,AutoFitBehavior:=1,DefaultTableBehavior:=1)

Реализация такого вызова на C++ выглядит так:

Exec(Procedure("ConvertToTable")

                  <<NamedParm("Separator","\t")

                  <<NamedParm("AutoFit",true)

                  <<NamedParm("AutoFitBehavior",1)

                  <<NamedParm("DefaultTableBehavior",1)

      );

Теперь, наконец, мы готовы реализовать рассматриваемый генератор отчетов на C++Builder. Ниже приводится полный текст обработчика события нажатия кнопки для запуска генератора:

void __fastcall TForm1::Button1Click(TObject *Sender)

{     Table1->DatabaseName="DBDEMOS"; Table1->TableName="country.db";

      Table1->Open();

      Variant W,D,S,PosBeg,PosEnd;

      try   // если Word запущен - подключиться к нему

      {     W = GetActiveOleObject("Word.Application");

      }

      catch(...)  // если нет - запустить

      {     W = CreateOleObject("Word.Application");

      }

//    W.Exec(PropertySet("Visible")<<true);

      W.OlePropertySet("Visible",true);

//    D=W.Exec(PropertyGet("Documents")).Exec(Function("Add"));

      D=W.OlePropertyGet("Documents").OleFunction("Add");

      S=W.OlePropertyGet("Selection");

//    S.OleProcedure("TypeText",WideString("Hello, World!\n"));

      S.Exec(Procedure("TypeText")<<WideString("Hello, World!\n"));

      PosBeg = S.Exec(PropertyGet("Start"));

      int i,j=0; WideString ws="№";

      for(i=0; i<Table1->FieldCount; i++)

            ws+="\t"+Table1->Fields->Fields[i]->FieldName;

      ws+="\n";

      S.OleProcedure("TypeText",ws);

      while(!Table1->Eof)

      {     ws=IntToStr(++j)+".";

            for(i=0; i<Table1->FieldCount; i++)

                  ws+="\t"+Table1->Fields->Fields[i]->AsString;

            ws+="\n";

            S.OleProcedure("TypeText",ws);

            Table1->Next();

      }

      PosEnd = S.Exec(PropertyGet("Start"));

      Table1->Close();

//    D.OleFunction("Range",PosBeg,PosEnd).Exec(

      D.Exec(Function("Range")<<PosBeg<<PosEnd).Exec(

            Procedure("ConvertToTable")

                  <<NamedParm("Separator","\t")

                  <<NamedParm("AutoFit",true)

                  <<NamedParm("AutoFitBehavior",1)

                  <<NamedParm("DefaultTableBehavior",1)

            );

}

Строки комментариев показывают альтернативные способы обращения к диспинтерфейсу.

Учебный пример клиента Microsoft Excel

Построим новый демонстрационный генератор отчетов по выборке из базы данных: теперь в качестве сервера автоматизации используем Excel. Такой выбор даже более естественен, чем предыдущий, поскольку, получив электронную таблицу с результатами запроса из базы данных, конечный пользователь сможет провести их дальнейшую обработку. Вновь напомним о необходимости изучения документации по программированию Microsoft Excel при разработке его клиентов автоматизации. В данном пособии мы имеем возможность лишь следующего краткого пояснения. Рассмотрим примерную схему иерархии внутренних объектов, экспортируемых Excel.

Рис. 2. Упрощенная схема иерархии объектов Excel

Эта схема показывает не иерархию наследования, а иллюстрирует способ доступа к тем или иным объекта. Итак, на вершине иерархии – объект Excel.Application. Свойствами объектов Excel могут являться так называемые коллекции объектов. Например, коллекция Workbooks является свойством объекта Excel.Application, при этом она содержит набор вложенных объектов – рабочих книг Excel, а те, в свою очередь, обладают свойством Worksheets, представляющим собой коллекцию рабочих листов, каждый из которых обладает свойством Cells, являющимся коллекцией ячеек. Доступ к конкретной ячейке, таким образом, можно получить через свойство Cells соответствующего рабочего листа:

  Sheet.Cells[1,1].Value := 'Hello, World!';

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

  Sheet.Range['A1'].Value := 'Hello, World!';

Достоинство второго способа проявляется в случае необходимости вывода в Excel больших массивов данных, что является обычной практикой. Ускорить такой вывод можно, используя следующий прием. Вместо последовательного вывода данных в каждую ячейку:

  for i:=1 to 10 do Sheet.Cells[1,i].Value := 'Hello, World!';

сначала формируется вариантный массив, после чего он выводится в диапазон ячеек одним обращением к диспинтерфейсу:

  Arr := VarArrayCreate([1,10],varVariant);

  for i:=1 to 10 do Arr[i] := 'Hello, World!';

  Sheet.Range['A1:J1'].Value := Arr;

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

Оба рассмотренные способа вывода массивов проиллюстрированы в примере реализации отклика на нажатие кнопки запуска генератора отчетов (остальные действия при разработке клиента полностью повторяют аналогичные действия, проделанные нами для реализации клиента Word):

procedure TForm1.Button1Click(Sender: TObject);

var   Exl,Book,Sheet,Arr: Variant; i,j: Integer;

begin

  with Table1 do begin

      DatabaseName:='DBDEMOS'; TableName:='country.db'; Open

  end;

  try // если Excel запущен - подключиться к нему

      Exl := GetActiveOleObject('Excel.Application');

  except    // если нет - запустить

      Exl := CreateOleObject('Excel.Application');

  end;

  Exl.Visible:=True;

  Book := Exl.WorkBooks.Add;

  Sheet := Book.ActiveSheet;

  Sheet.Range['A1'].Value := 'Hello, World!';

  Sheet.Range['A2'].Value := '№';

  j:=0;

  for i:=0 to Table1.FieldCount-1 do

      Sheet.Cells[2,i+2].Value:=Table1.Fields.Fields[i].FieldName;

  Arr := VarArrayCreate([0,Table1.FieldCount],varVariant);

  with Table1 do while not Eof do begin

      j:=j+1;

      Arr[0] := IntToStr(j);

      for i:=0 to FieldCount-1 do

            Arr[i+1] := Fields.Fields[i].AsString;

      Sheet.Range['A'+IntToStr(j+2)+':'+

            Chr(Ord('A')+FieldCount)+IntToStr(j+2)].Value := Arr;

      Next

  end;

  Table1.Close

end;

Заметим, что при выводе шапки таблицы каждая ячейка Excel заполняется индивидуально, а для вывода данных заполняется вариантный массив, который затем используется для передачи Excel строки таблицы целиком:

Рис. 3.  Отчет в документе Microsoft Excel

Для полноты изложения материала приведем текст программы на C++ для разработчиков клиента в среде программирования Borland C++Builder:

void __fastcall TForm1::Button1Click(TObject *Sender)

{     Table1->DatabaseName="DBDEMOS"; Table1->TableName="country.db";

      Table1->Open();

      Variant Exl,Book,Sheet,Arr;

      try         // если Excel запущен - подключиться к нему

      {     Exl = GetActiveOleObject("Excel.Application");

      }

      catch(...)  // Excel не запущен - запустить его

      {     Exl = CreateOleObject("Excel.Application");

      }

      Exl.OlePropertySet("Visible", true);

      Book = Exl.OlePropertyGet("WorkBooks").OleFunction("Add");

      Sheet = Book.OlePropertyGet("ActiveSheet");

      Sheet.OlePropertyGet("Range","A1").

            OlePropertySet("Value","Hello, World!");

      Sheet.OlePropertyGet("Range","A2").

            OlePropertySet("Value","№");

      int i,j=0;

      for(i=0; i<Table1->FieldCount; i++)

        Sheet.OlePropertyGet("Cells",2,i+2).

            OlePropertySet("Value",Table1->Fields->Fields[i]->FieldName);

      int Bounds[]={0,Table1->FieldCount};

      Arr = VarArrayCreate(Bounds,1,varVariant);

      while(!Table1->Eof)

      {     Arr.PutElement(IntToStr(++j),0);

            for(i=0; i<Table1->FieldCount; i++)

                  Arr.PutElement(Table1->Fields->Fields[i]->AsString,i+1);

            Sheet.OlePropertyGet("Range",Sheet.OlePropertyGet("Cells",j+2,1),

                  Sheet.OlePropertyGet("Cells",j+2,Table1->FieldCount+1)).

                  OlePropertySet("Value", Arr);

            Table1->Next();

      }

      Table1->Close();

}

Программа использует описанные в предыдущем разделе механизмы управления диспинтерфейсом. Новым здесь является лишь использование вариантного массива, для создания которого функцией VarArryCreate потребовался вспомогательный массив Bounds, описывающий его размерности.

Использование библиотеки типов для раннего связывания

В предыдущих разделах достаточно подробно рассмотрен способ создания клиентов с использованием типа Variant, основанного на механизме динамического (позднего) связывания. При таком способе клиент (контроллер автоматизации) явно или неявно использует метод IDispatch::GetIDsOfNames, а возможно также другие методы IDispatch, для генерирования динамического запроса к объекту во время выполнения. Данный способ наиболее прост в использовании изнутри Delphi (как и C++Builder), поскольку все подробности механизма скрыты от программиста. Тем не менее, он является наименее производительным: достаточно представить себе многократные вызовы GetIDsOfNames для удаленного сервера автоматизации, передаваемые по сети! Высокопроизводительная альтернатива – раннее связывание на этапе компиляции или компоновки распределенного приложения. Данный механизм предполагает, что на момент создания клиента полностью известна информация о типе используемых объектов автоматизации: список поддерживаемых интерфейсов и их методов, параметры каждого метода, типы свойств объектов и параметров методов и многое другое. Откуда программист клиента (или соответствующая среда программирования) может получить всю эту информацию? Один из источников нам уже известен – это документация, сопровождающая сервер автоматизации. Тем не менее, в таком серьезном деле, как раннее связывание, нельзя слепо полагаться на добросовестность авторов документации. Во-первых, предоставляемой информации может оказаться недостаточно, во-вторых, этой неформализованной информацией не сможет воспользоваться среда программирования для автоматизации процессов сборки распределенного приложения. Более надежный источник – библиотека типов, сопровождающая практически любой сколько-нибудь серьезный сервер автоматизации.

Библиотека типов (Type Library) – независимое от языка программирования средство исчерпывающего документирования COM-объекта. Формат библиотеки типов стандартизован в COM. Поставка библиотеки типов может осуществляться разными способами. Например, ее содержимое может быть интегрировано в COM-сервер в качестве ресурса или распространяться независимо в отдельных файлах с расширением .TLB или .OLB . Последнее расширение традиционно для поставки библиотек типов приложений Microsoft Office.

Изучение библиотеки типов Delphi-программистом

Стандартный способ спецификации библиотек типов – язык Microsoft IDL (Interface Definition Language). По данной спецификации компилятор MIDL может сгенерировать библиотеку в стандартном двоичном формате, и именно этот последний формат – обязателен для комплекта поставки сервера автоматизации. Исходный текст на IDL, как правило, не поставляется. Чтобы восполнить этот пробел современные интегрированные среды программирования предоставляют два механизма использования доступной информации о типе:

·        просмотр и редактирование библиотеки типов при помощи специальной утилиты – редактора библиотеки типов;

·        импорт библиотеки типов в проект клиентского приложения.

Оба механизма обеспечены как в среде Delphi, так и в C++Builder. Встроенный редактор библиотеки типов позволяет осуществлять просмотр и редактирование библиотеки, доступной в двоичном формате. Более того, при желании можно автоматически сгенерировать исходный текст описания библиотеки типов на IDL (осуществить экспорт библиотеки типов). Процедура импорта библиотеки типов заключается, упрощенно говоря, в генерации необходимой совокупности модулей, присоединяемых к проекту и делающих доступной информацию о типах на этапе компиляции. Другими словами, используя библиотеку типов в двоичном формате, среда программирования автоматически генерирует исходный текст на соответствующем языке программирования (Object Pascal или C++) специальных модулей, на которые можно будет ссылаться из остальных модулей, составляющих проект. Такая процедура имеет общее название (из терминологии распределенных вычислений) – отображение к языку программирования.

Изучение библиотеки типов незнакомого сервера автоматизации мы рекомендуем начинать с ее просмотра в редакторе библиотеки типов. Поскольку речь идет о Delphi, а редактор интегрирован в среду программирования, проще всего открыть библиотеку типов во время разработки клиентского проекта: меню FileOpen, и указать файл библиотеки в двоичном формате (можно файл *.EXE приложения для Windows). Давайте, как и раньше экспериментировать с приложениями Microsoft Office. В данном случае важна версия: дальнейшие конкретные примеры рассматривают версию 2000, однако, общая методика от версии, разумеется, не зависит.

Итак, начнем с изучения библиотеки типов Microsoft Word, открыв файл MSWORD9.OLB, в котором она размещена. Первое, что нужно сделать – это узнать, какие объекты автоматизации экспортирует сервер. В библиотеке типов каждый объект описан при помощи так называемого сопряженного класса (CoClass). В нашем случае можно обнаружить, что сервер реализует целый ряд объектов: Global, Application, Document, Font и др. (см. рисунок ниже). Для объектов, которые предполагается использовать, необходимо выяснить, какие интерфейсы каждый из них реализует. Например, объект Application, без использования которого невозможно обойтись, реализует показанные на рисунке интерфейсы, причем, интерфейс _Application описан, как интерфейс по умолчанию. Последняя информация чрезвычайно важна для программистов, планирующих связываться с объектом через интерфейс IDispatch, поэтому на данный факт также целесообразно обращать внимание в первую очередь.

Рис. 4.  Библиотека типов Microsoft Word в редакторе

После изучения необходимых сопряженных классов можно переходить к реализуемым ими интерфейсам. Вкладка Text редактора содержит автоматически сгенерированный фрагмент описания библиотеки типа на стандартном языке IDL, причем, представленный фрагмент относится к выбранному на дереве слева узлу. Например, так выглядит описание интерфейса _Application (в сильном сокращении):

[

  uuid(00020970-0000-0000-C000-000000000046),

  version(8.1),

  helpcontext(0x00000970),

  hidden,

  dual,

  nonextensible,

  oleautomation

]

 interface _Application: IDispatch

{     . . .

  [propget, id(0x00000006), helpcontext(0x09700006)]

  HRESULT _stdcall Documents([out, retval] Documents ** prop );

  [propget, id(0x00000002), helpcontext(0x09700002)]

  HRESULT _stdcall ActiveDocument([out, retval] Document ** prop );

      . . .

  [id(0x00000451), helpcontext(0x09700451)]

  HRESULT _stdcall Quit([in, optional] VARIANT * SaveChanges,

    [in, optional] VARIANT * OriginalFormat,

    [in, optional] VARIANT * RouteDocument );

  [id(0x00000168), helpcontext(0x09700168)]

  HRESULT _stdcall Move([in] long Left, [in] long Top );

  [id(0x00000169), helpcontext(0x09700169)]

  HRESULT _stdcall Resize([in] long Width, [in] long Height );

      . . .

};

Программист, предполагающий использовать данный интерфейс, должен отметить следующее. Во-первых, это дуальный (dual) интерфейс, доступ к которому возможен двумя способами: как к диспинтерфейсу через IDispatch и как к обычному интерфейсу с виртуальной таблицей. Далее – интерфейс совместим с OLE-автоматизацией (oleautomation), что позволяет при вызове его методов использовать стандартный маршалинг параметров при помощи упаковки в Variant. Наконец, описания свойств и методов интерфейса содержат всю необходимую для их использования информацию: диспетчерские идентификаторы (DISPID) специфицированы ключевым словом id, типы свойств (в нашем случае Documents и ActiveDocument) снабжены спецификаторами типов и всеми необходимыми атрибутами, методы (у нас – Quit, Move и Resize) – списками параметров вызова.

После изучения содержимого библиотеки типов в редакторе можно переходить к процедуре ее импорта в проект разрабатываемого клиентского приложения. Запускается процедура из меню ProjectImport Type Library. В диалоговом окне «Import Type Library» необходимо выбрать нужную библиотеку из готового списка, перечисляющего установленные на вашем компьютере серверы автоматизации (в нашем примере – Microsoft Word). Либо указать конкретный файл библиотеки (MSWORD9.OLB), нажав кнопку «Add». Поскольку мы хотим ограничиться лишь созданием импортированного модуля, снимем флажок «Generate Component Wrapper», зададим каталог для записи файлов генерируемого модуля в строке ввода «Unit dir name» и нажмем кнопку «Create Unit». По завершении процедуры импорта, выполнение которой может занять некоторое время из-за больших размеров генерируемых файлов, к проекту будет присоединен вновь созданный модуль Word_TLB.pas.

Важное замечание. Описанная процедура импорта может потребовать деинсталляции компонентов вкладки «Servers» палитры, если таковые установлены на вашем компьютере. Дело в том, что все эти компоненты получены именно путем импортирования библиотек типов соответствующих серверов автоматизации и среда Delphi не считает возможным повторять эту процедуру. Если вы все-таки хотите ее пройти, вам необходимо вызвать диалог из меню ComponentInstall Packages и снять флажок напротив пакета, в который соответствующие компоненты были установлены.

Одновременно с импортированным модулем Word_TLB.pas могут быть созданы и другие файлы. Например, в нашем случае их два: Office_TLB.pas и VBIDE_TLB.pas. Последнее означает, что импортированная библиотека типов содержала ссылки на другие файлы, следовательно, импортированный в проект модуль – также содержит соответствующие ссылки:

uses Office_TLB, VBIDE_TLB;  // в файле Word_TLB.pas

Включать эти дополнительные модули в проект не нужно, поскольку разрабатываемые вами модули будут использовать описания только из Word_TLB.pas. Не забудьте для этого вставить соответствующие ссылки:

uses Word_TLB;               // в ваших модулях

Рассмотрим импортированный модуль более подробно. Заметим, что при просмотре его содержимого неоценимую помощь оказывает дерево «Code Explorer», поскольку объем модуля достаточно велик (более мегабайта на моем компьютере). Прежде всего, необходимо обратить внимание на ошибки импортирования, предупреждения о которых содержатся в самом начале модуля в виде комментариев. Вот некоторые из них:

// Errors:

//   Hint: Symbol 'Application' renamed to 'WordApplication'

//   Hint: Symbol 'Document' renamed to 'WordDocument'

//   Hint: Symbol 'Font' renamed to 'WordFont'

//   Hint: Member 'Repeat' of '_Application' changed to 'Repeat_'

//   Hint: Member 'Type' of '_Document' changed to 'Type_'

//   Hint: Member 'End' of 'Range' changed to 'End_'

//   Hint: Member 'Case' of 'Range' changed to 'Case_'

//   Hint: Parameter 'End' of Range.SetRange changed to 'End_'

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

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

// *********************************************************************//

// Declaration of CoClasses defined in Type Library                      

// (NOTE: Here we map each CoClass to its Default Interface)             

// *********************************************************************//

  Global = _Global;

  WordDocument = _Document;

  WordFont = _Font;

  WordParagraphFormat = _ParagraphFormat;

  WordOLEControl = _OLEControl;

  WordLetterContent = _LetterContent;

  WordApplication = _Application;

Так, рассмотренный выше сопряженный класс Application, переименованный в процессе импорта в WordApplication, реализует интерфейс по умолчанию с именем _Application. Помимо этого в самом конце текста модуля находятся описания специальных классов (Object Pascal), по одному для каждого импортированного сопряженного класса, которые имеют достаточно простой вид. Например, для класса WordApplication создано следующее описание, снабженное исчерпывающим комментарием:

// *********************************************************************//

// The Class CoWordApplication provides a Create and CreateRemote method to         

// create instances of the default interface _Application exposed by             

// the CoClass WordApplication. The functions are intended to be used by            

// clients wishing to automate the CoClass objects exposed by the        

// server of this typelibrary.                                           

// *********************************************************************//

  CoWordApplication = class

    class function Create: _Application;

    class function CreateRemote(const MachineName: string): _Application;

  end;

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

 

 

  _Application = interface(IDispatch)

    ['{00020970-0000-0000-C000-000000000046}']

...

    function  Get_Documents: Documents; safecall;

    function  Get_Windows: Windows; safecall;

    function  Get_ActiveDocument: WordDocument; safecall;

    function  Get_ActiveWindow: Window; safecall;

    function  Get_Selection: Selection; safecall;

...

    property Documents: Documents read Get_Documents;

    property Windows: Windows read Get_Windows;

    property ActiveDocument: WordDocument read Get_ActiveDocument;

    property ActiveWindow: Window read Get_ActiveWindow;

    property Selection: Selection read Get_Selection;

...

  end;

Это описание – обычно для программы на Object Pascal. Следует лишь обратить внимание на ключевое слово safecall, специфицирующее описание любого метода интерфейса. В связи с этим необходимо рассмотреть соглашение о безопасном вызове, принятое в Object Pascal. Рассматривая библиотеку типов в редакторе, вы уже, наверное, обратили внимание на специальный тип возвращаемого значения методов. Дело в том, что для серверов автоматизации существует жесткое ограничение: все их методы должны возвращать значение типа HResult. Этот тип определен в COM модели и дает значение, которое свидетельствует о том, успешно ли завершилось выполнение операции или нет, причем в последнем случае переданное значение содержит код ошибки. Другие данные, полученные в результате работы метода, возвращаются через выходные параметры (с модификатором out). Таким образом, если бы Object Pascal не предусматривал ключевое слово safecall, импортированное описание метода Get_Documents, например, выглядело бы так:

    function  Get_Documents(var Docs: Documents): HResult;

Вызывать такой метод было бы неудобно. Директива safecall, активирующая соглашение о безопасном вызове, заставляет Delphi взять на себя ответственность за анализ возвращенного кода HResult. При этом генерируется исключение при получении кода ошибки. Программист же, со своей стороны, пишет удобный код вызова, соответствующий импортированному описанию.

Управление Microsoft Word через импортированные интерфейсы

Здесь мы будем модифицировать код разработанного ранее генератора отчетов по выборкам из базы данных. Напомним, что этот код использует динамическое связывание, скрытое от программиста внутри встроенного типа Variant. Выше сказано, что реализация программируемости через Variant хотя и удобна, но малоэффективна. Теперь, изучив библиотеку типов сервера Microsoft Word, мы получаем возможность использования одного из двух более эффективных способов управления, задействующих механизм раннего связывания. Наиболее эффективным из них является вызов методов COM‑интерфейса напрямую через виртуальную таблицу. Этот способ рассматривается в данном разделе. А в следующем рассмотрен второй способ – обращение к свойствам и методам диспинтерфейса через IDispatch:Invoke.

Итак, начнем с редактирование описаний предыдущего примера, использовавших Variant. Теперь мы будем использовать либо типы интерфейсов, описанные в импортированном модуле библиотеки типов, либо OleVariant – специальный тип Object Pascal, тоже представляющий вариант, но только лишь для типов, совместимых с COM:

var W:_Application; D:_Document; S: Selection;

    PosBeg,PosEnd: OleVariant;

Заметьте, мы будем использовать _Application и _Document, которые являются интерфейсами по умолчанию для соответствующих объектов автоматизации. Код, инициирующий связывание с сервером преобразуем к виду:

  try // если Word запущен - подключиться к нему

      W := GetActiveOleObject(

        ClassIDToProgID(CLASS_WordApplication))as _Application;

  except    // если нет - запустить

      W:=CoWordApplication.Create;

  end;

Здесь в иллюстративных целях используется функция ClassIDToProgID, позволяющая извлечь из реестра программный идентификатор ProgId по заданному идентификатору класса CLSID. Поскольку с одной стороны, отображение ProgIdCLSID в реестре сохраняется при инсталляции сервера, а с другой стороны, значение параметра CLASS_WordApplication описано в импортированном модуле библиотеки типов, мы гарантированно подключаемся именно к тому серверу, библиотека типов которого использована в проекте. В случае неудачи соединения сервер запускается при помощи класса CoWordApplication, образованного в процессе импорта сопряженного класса библиотеки типов.

Последнее, о чем следует сказать перед тем как привести исходный код целиком, это о способе вызова методов с необязательными параметрами. Рассмотрите следующий прием:

D:=W.Documents.Add(EmptyParam,EmptyParam,EmptyParam,EmptyParam);

Если вернуться к просмотру библиотеки типов, можно заметить, что метод Add имеет четыре необязательных параметра, для которых сервер, по-видимому, предусматривает значения по умолчанию. Если нас устраивают эти значения, можно в качестве параметров передать EmptyParam. А теперь – обещанный полный текст генератора отчета, переработанный в соответствии с изложенными принципами:

procedure TForm1.Button1Click(Sender: TObject);

var W:_Application; D:_Document; S: Selection;

    PosBeg,PosEnd,v1,v2,v3: OleVariant;

    i,j: Integer; ws: WideString;

begin

  with Table1 do begin

      DatabaseName:='DBDEMOS'; TableName:='country.db'; Open

  end;

  try // если Word запущен - подключиться к нему

      W := GetActiveOleObject(

        ClassIDToProgID(CLASS_WordApplication))as _Application;

  except    // если нет - запустить

      W:=CoWordApplication.Create;

  end;

  W.Visible := True;

  D:=W.Documents.Add(EmptyParam,EmptyParam,EmptyParam,EmptyParam);

  S:=W.Selection;

  S.TypeText('Hello, World!'#13);

  PosBeg := S.Start;

  j:=0; ws:='№';

  for i:=0 to Table1.FieldCount-1 do

      ws:=ws+#9+Table1.Fields.Fields[i].FieldName;

  ws:=ws+#13;

  S.TypeText(ws);

  with Table1 do while not Eof do begin

      j:=j+1;

      ws:=IntToStr(j)+'.';

      for i:=0 to FieldCount-1 do

        ws:=ws+#9+Fields.Fields[i].AsString;

      ws:=ws+#13;

      S.TypeText(ws);

      Next

  end;

  PosEnd := S.Start;

  Table1.Close; v1:=#9; v2:=True; v3:=1;

  D.Range(PosBeg,PosEnd).ConvertToTable(v1,EmptyParam,EmptyParam,

    EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,

    EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,

    v2,v3,v3)

end;

Если теперь построить проект и запустить приложение, результат его работы будет тем же, что и при работе клиента через Variant. Тем не менее, затраченные усилия окупаются как в процессе разработки приложения (раннее связывание безопаснее, поскольку выявляются ошибки при компиляции и компоновке), так и в режиме выполнения (эффективен доступ к сервисам объектов автоматизации).

Раннее связывание через диспинтерфейс

В данном разделе рассмотрен способ обращения к свойствам и методам диспинтерфейса через IDispatch:Invoke, использующий, также как предыдущий, информацию из библиотеки типов и раннее связывание. Оба эти механизма управления сервером Microsoft Word стали возможными, благодаря тому, что большинство экспортируемых интерфейсов являются дуальными. Способ, описываемый ниже, по эффективности занимает промежуточное положение между обращением через виртуальную таблицу интерфейса (как показано в предыдущем разделе) и управлением с использованием Variant (динамическое связывание через диспинтерфейс). Поэтому материал, приводимый ниже, необходим как иллюстрация работы с серверами автоматизации, имеющими только диспинтерфейсы: при наличии дуальных интерфейсов предпочтение, очевидно, следует отдавать их виртуальным таблицам.

Вернувшись к рассмотрению модуля Word_TLB, построенного в результате импорта библиотеки типов Microsoft Word, можно заметить, что все интерфейсы, импортированные из дуальных интерфейсов библиотеки, имеют «двойников». Например, использованный ранее интерфейс _Application (часть его описания приводится выше по тексту) сопровождается следующим фрагментом (в сильном сокращении):

  _ApplicationDisp = dispinterface

    ['{00020970-0000-0000-C000-000000000046}']

...

    property Documents: Documents readonly dispid 6;

    property Windows: Windows readonly dispid 2;

    property ActiveDocument: WordDocument readonly dispid 3;

    property ActiveWindow: Window readonly dispid 4;

    property Selection: Selection readonly dispid 5;

    property Visible: WordBool dispid 23;

...

    procedure Move(Left: Integer; Top: Integer); dispid 360;

    procedure Resize(Width: Integer; Height: Integer); dispid 361;

    function  CheckGrammar(const String_: WideString): WordBool;

      dispid 323;

...

  end;

Отметим, что данный интерфейс идентифицирован тем же самым глобальным идентификатором (IID), что и _Application. Кроме того, многие его свойства и методы – также дублируют соответствующие возможности «двойника» _Application. Только в данном случае их описания вместо директив safecall снабжены диспетчерскими идентификаторами (dispid), заимствованными из библиотеки типов. Подобные двойники, описанные ключевым словом dispinterface, есть и у всех остальных дуальных интерфейсов: _DocumentDisp, SelectionDisp и т.д. Именно они делают для клиента на Object Pascal вызовы через IDispatch:Invoke прозрачными.

Используем рассмотренный прием для модификации нашего учебного генератора отчетов. Вместо _Application используем его «двойника»:

var W:_ApplicationDisp;

При подключении к серверу требуются дополнительные операции приведения к использованному типу интерфейса:

  try // если Word запущен - подключиться к нему

      W := GetActiveOleObject(

        ClassIDToProgID(CLASS_WordApplication))as _ApplicationDisp;

  except    // если нет - запустить

      W:=CoWordApplication.Create as _ApplicationDisp;

  end;

  W.Visible := True;

Последняя строка иллюстрирует доступ к свойству объекта автоматизации через его диспинтерфейс.

Клиент Microsoft Excel с механизмом раннего связывания

Усовершенствуем предложенным способом разработанный ранее генератор отчетов в книге Microsoft Excel. Начнем с того, что изучим библиотеку типов, поставляемую в файле excel9.olb, сначала в редакторе библиотек, а затем – импортируем ее в состав проекта клиентского приложения. Все действия при этом аналогичны только что описанным. Заменим приведенным ниже фрагментом код отклика на событие запуска генератора, а затем рассмотрим особенности программирования по сравнению с методом доступа через Variant:

procedure Tform1.Button1Click(Sender: Tobject);

var   Exl:_Application; Book:_WorkBook; Sheet:_WorkSheet;

      i,j: Integer; Arr: Variant;

begin

  with Table1 do begin

      DatabaseName:='DBDEMOS'; TableName:='country.db'; Open

  end;

  try // если Excel запущен – подключиться к нему

      Exl := GetActiveOleObject(

        ClassIDToProgID(CLASS_ExcelApplication))as _Application;

  except    // если нет – запустить

      Exl:=CoExcelApplication.Create;

  end;

  Exl.Visible[0]:=True;

  Book := Exl.WorkBooks.Add(EmptyParam,0);

  Sheet := Book.ActiveSheet as _WorkSheet;

  Sheet.Range['A1',EmptyParam].Value := 'Hello, World!';

  Sheet.Range['A2',EmptyParam].Value := '№';

  j:=0;

  for i:=0 to Table1.FieldCount-1 do

      Sheet.Cells.Item[2,i+2].Value :=

        Table1.Fields.Fields[i].FieldName;

  Arr := VarArrayCreate([0,Table1.FieldCount],varVariant);

  with Table1 do while not Eof do begin

      j:=j+1;

      Arr[0] := IntToStr(j);

      for i:=0 to FieldCount-1 do

        Arr[i+1] := Fields.Fields[i].AsString;

      Sheet.Range['A'+IntToStr(j+2),

        Chr(Ord('A')+FieldCount)+IntToStr(j+2)].Value := Arr;

      Next

  end;

  Table1.Close

end;

Итак, соединение с сервером реализовано так же, как и сервером Word. Но вызывает удивление то, что свойство Visible является векторным. Действительно, при изучении библиотеки типов можно обратить внимание на то, что многие описанные в ней методы и свойства требуют передачи специального входного параметра lcid (local ID) – 32‑битового идентификатора локализации, используемого в системе языковой поддержки Win32 National Language Support. Рассмотрите, например, фрагмент IDL‑описания интерфейса _Application, включающий метод SaveWorkspace и свойства Visible и Width:

[

  uuid(000208D5-0000-0000-C000-000000000046),

  helpcontext(0x00020001),

  dual,

  oleautomation

]

 interface _Application: IDispatch

{

      ...

  [id(0x000000D4), helpcontext(0x000100D4)]

  HRESULT _stdcall SaveWorkspace([in, optional] VARIANT Filename,

    [in, lcid] long lcid );

      ...

  [propget, id(0x0000022E), helpcontext(0x0001022E)]

  HRESULT _stdcall Visible([in, lcid] long lcid,

    [out, retval] VARIANT_BOOL * RHS );

  [propput, id(0x0000022E), helpcontext(0x0001022E)]

  HRESULT _stdcall Visible([in, lcid] long lcid,

    [in] VARIANT_BOOL RHS );

      ...

  [propget, id(0x0000007A), helpcontext(0x0001007A)]

  HRESULT _stdcall Width([in, lcid] long lcid,

    [out, retval] double * RHS );

  [propput, id(0x0000007A), helpcontext(0x0001007A)]

  HRESULT _stdcall Width([in, lcid] long lcid,

    [in] double RHS );

      ...

};

Несколько по иному, нежели в программе с Variant, организована работа со свойствами Cells и Range интерфейса _Worksheet. Приведем их описания на IDL из библиотеки типов:

interface _Worksheet: IDispatch

{     ...

  [propget, id(0x000000EE), helpcontext(0x000100EE)]

  HRESULT _stdcall Cells([out, retval] Range ** RHS );

      ...

  [propget, id(0x000000C5), helpcontext(0x000100C5)]

  HRESULT _stdcall Range([in] VARIANT Cell1,

    [in, optional] VARIANT Cell2, [out, retval] Range ** RHS );

      ...

};

Как видите, оба эти свойства возвращают значения одного и того же типа. При этом, для того, чтобы использовать значение Cells как указатель на коллекцию ячеек, необходимо явное использования двумерного векторного свойства Item. Здесь не проходит «трюк», который мы использовали в программе с Variant:

Sheet.Cells[2,i+2].Value:=Table1.Fields.Fields[i].FieldName;

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

Свойство Range имеет два входных параметра, из которых последний – необязательный. Мы в своей программе используем оба варианта: как с явным заданием второго параметра, так и с его пропуском, указав EmptyParam вместо него.

Импортирование библиотеки типов в проект C++Builder

Среда программирования C++Builder реализует все описанные выше возможности по использованию библиотеки типов в целях создания контроллеров автоматизации с ранним связыванием. Помимо основных преимуществ, связанных собственно с использованием раннего связывания, в данном случае получается гораздо более компактный и удобочитаемый исходный код: отпадает необходимость в многочисленных вызовах методов класса Variant OleProcedure, OleFunction, OlePropertyGet, OlePropertySet и т.п.

Как уже говорилось, разработчики C++Builder для реализации возможности COM‑программирования везде, где это возможно, обходятся стандартными средствами языка C++, избегая расширения стандарта в угоду упрощению синтаксиса. В результате ими написано огромное количество программного обеспечения на C++, облегчающего COM‑программирование. Причем оно открыто для понимания программисту, использующему соответствующие заголовочные файлы. В этом смысле программист на C++ находится в более выгодном положении, нежели программист на Object Pascal.

Теперь приступим к решению поставленной задачи, конкретно, создадим клиентское приложение, использующее импорт библиотеки типов. Все подготовительные процедуры описаны в предыдущих разделах. Также как в среде Delphi, можно открыть библиотеку типов в редакторе и просмотреть ее содержимое. Если библиотека типов Microsoft Excel, поставляемая в файле excel9.olb, вами уже изучена в достаточной степени, можно переходить к процедуре ее импортирования в состав проекта. В результате импорта, в проект будет добавлен модуль Excel_TLB.cpp с заголовочным файлом Excel_TLB.h. Инструмент Class Explorer поможет вам в поиске нужных фрагментов при просмотре этих файлов, поскольку они имеют поистине впечатляющие размеры (более восьми с половиной мегабайт на моем компьютере)!

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

Прежде всего, рассмотрим файл Excel_TLB.cpp, который имеет относительно небольшой размер. Он полезен, поскольку именно он в виде комментариев содержит сообщения об ошибках импорта идентификаторов библиотеки:

// Errors:

//   Hint: Symbol 'Windows' renamed to 'Windoz'

//   Hint: Symbol 'Application' renamed to 'ExcelApplication'

//   Hint: Symbol 'Chart' renamed to 'ExcelChart'

//   Hint: Symbol 'Worksheet' renamed to 'ExcelWorksheet'

//   Hint: Symbol 'Workbook' renamed to 'ExcelWorkbook'

      ...

Далее следуют определения глобальных идентификаторов всех объектов библиотеки типов (самой библиотеки, интерфейсов и сопряженных классов) в виде определений констант:

const GUID LIBID_Excel = {0x00020813, 0x0000, 0x0000,{ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46} };

const GUID IID_Adjustments = {0x000C0310, 0x0000, 0x0000,{ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46} };

Просмотр заголовочного файла Excel_TLB.h может занять существенно более продолжительное время. Мы сосредоточим свое внимание на тех конструкциях, в которые превратились импортированные интерфейсы. Ниже приведен фрагмент, поясняющий некоторые особенности процедуры импорта хорошо знакомого нам дуального интерфейса _Application:

interface _Application  : public IDispatch

{     . . .

  __property   Excel_tlb::WorkbooksPtr Workbooks =

      {read = get_Workbooks};

      . . .

};

      . . .

template <class T /* _Application */ >

class TCOM_ApplicationT : public TComInterface<_Application>,

    public TComInterfaceBase<IUnknown>

{

      . . .

};

typedef TCOM_ApplicationT<_Application> TCOM_Application;

      . . .

template<class T>

class _ApplicationDispT : public TAutoDriver<_Application>

{

public:

      . . .

  HRESULT BindDefault()

  {   return OLECHECK(Bind(CLSID_ExcelApplication));

  }

  HRESULT BindRunning()

  {   return BindToActive(CLSID_ExcelApplication);

  }

      . . .

};

typedef _ApplicationDispT<_Application> _ApplicationDisp;

Мы видим здесь описание сразу пяти объектов: интерфейса _Application, класса TCOM_Application, построенного по описанному здесь же шаблону TCOM_ApplicationT, шаблон классов _ApplicationDispT и построенный по нему класс _ApplicationDisp. В результате программист получил в распоряжение два рабочих инструмента для программирования импортированного интерфейса: классы TCOM_Application и _ApplicationDisp, инкапсулирующие соответственно интерфейс с виртуальной таблицей и диспинтерфейс. Это значит, что для программиста на C++ оба рассмотренные выше способа доступа к дуальному интерфейсу столь же легко реализуемы, как и для программиста на Object Pascal.

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

_ApplicationDisp Exl;

if(!SUCCEEDED(          // если Excel работает -

    Exl.BindRunning())) // подключиться к нему;

  Exl.BindDefault();    // не запущен - запустить его

Просматривая заголовочный файл Excel_TLB.h, можно заметить также, что многие свойства и методы возвращают типы, идентификаторы которых имеют один и тот же суффикс Ptr. Так в приведенном выше фрагменте свойство Workbooks интерфейса _Application имеет тип WorkbooksPtr. Описания всех этих типов тоже можно найти в файле Excel_TLB.h, например:

interface DECLSPEC_UUID(

      "{000208DA-0000-0000-C000-000000000046}") _Workbook;

typedef TComInterface<_Workbook, &IID__Workbook> _WorkbookPtr;

interface DECLSPEC_UUID(

      "{000208DB-0000-0000-C000-000000000046}") Workbooks;

typedef TComInterface<Workbooks, &IID_Workbooks> WorkbooksPtr;

Таким образом, все эти типы также предназначены для доступа к виртуальной таблице соответствующих интерфейсов. В заключение данного руководства обратим внимание на удобную особенность классов – наследников TAutoDriver. Если этот наследник инкапсулирует дуальный интерфейс, он позволяет доступ к методам последнего не только через IDispatch::Invoke, но и напрямую – через виртуальную таблицу. Для реализации такого прямого доступа в шаблоне TAutoDriver соответствующим образом перегружена операция обращения по указателю -> . Таким образом, имея например, объект класса _ApplicationDisp, мы можем использовать любой из двух способов доступа к дуальному интерфейсу _Application:

Exl.Visible = true;        // Visible - свойство _ApplicationDisp

_WorkbookDisp Book =

    Exl->Workbooks->Add(); // Workbooks - свойство _Application

Заключение: основные шаги при разработке контроллеров автоматизации

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

·        использовать механизм позднего (динамического) связывания через методы IDispatch;

·        импортировать библиотеку типов сервера и использовать механизм раннего связывания через виртуальные таблицы интерфейсов;

·        использовать вызовы диспинтерфейсов через IDispatch::Invoke при раннем связывании, также импортировав библиотеку типов.

Первый из рассматриваемых способов наиболее прост в реализации, но наименее эффективен в режиме выполнения. В данном случае в процессе разработки контроллера автоматизации в средах программирования Delphi или C++Builder необходимо:

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

2.      Написать текст программы контроллера, в котором интерфейсы объявить либо как экземпляры встроенного типа Variant (Object Pascal), либо как объекты класса Variant (С++). В последнем случае для доступа к свойствам и методам интерфейса использовать специальные методы класса Variant.

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

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

2.      Импортировать библиотеку типов в проект клиентского приложения. Процедура импорта сгенерирует модуль описания библиотеки типов на соответствующем языке программирования (Object Pascal или C++), после чего на него можно ссылаться из собственных модулей проекта (директивы uses или #include соответственно).

3.      Изучить части импортированного модуля, ответственные за описания используемых сопряженных классов и интерфейсов библиотеки типов. Обратить внимание на сообщения об ошибках импортирования, включенные в текст модуля в виде комментариев.

4.      Написать текст программы контроллера, в котором интерфейсы объявить как экземпляры соответствующих классов импортированного модуля. Доступ к интерфейсам реализовать по правилам, предусмотренным использованными классами и их описаниями.

Последний из способов управления серверами автоматизации, рекомендуется для доступа к диспинтерфейсам, которые сервер не реализует как дуальные. Этот способ предполагает объявление интерфейса в программе клиента при помощи специальных классов, описанных в модуле импортированной библиотеки типов, имеющих специфические особенности для среды программирования (Delphi или C++Builder).