Предыдущая Оглавление Следующая

Создание локальных серверов и контроллеров автоматизации

Понятие автоматизации.

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

Отметим, однако, что программирование с помощью макроязыков имеет свои недостатки, так как не существует спецификаций, которым должны подчиняться макроязыки. Соответственно в общем случае у каждого программируемого приложения имеется свой макроязык, отличный от макроязыков других программируемых приложений (исключением, пожалуй, являются приложения Microsoft Office, где в качестве макроязыка используется Visual Basic for Applications - подмножество Visual Basic).

Более удобной реализацией программируемости настольных приложений было бы наличие в них возможности предоставлять свои сервисы другим приложениям с помощью универсального механизма, не зависящего от встроенных макроязыков и позволяющего, в частности, использовать обычные языки программирования. Именно для этой цели и предназначен механизм, называемый автоматизацией (Automation; ранее этот механизм назывался OLE Automation). В этом случае приложение, предоставляющее другим приложениям тот или иной сервис, использует для этой цели интерфейсы содержащихся внутри его адресного пространства СОМ-объектов, и называется сервером автоматизации. Приложение, использующее сервис, называется контроллером автоматизации и может быть написано с помощью подавляющего большинства современных средств разработки. Отметим, что серверами автоматизации являются, в частности, такие популярные приложения, как Microsoft Office (Word, Excel, Power Point), Seagate Crystal Reports, Microsoft Internet Explorer и даже сама оболочка Windows 95/98/NT.

Сервер автоматизации может выполняться в адресном пространстве клиента. В этом случае он называется внутренним (in-process) сервером и реализуется в виде библиотеки (DLL).

Помимо этого, сервер автоматизации может выполняться в собственном адресном пространстве, отличном от адресного пространства контроллера. В этом случае он называется локальным (out-of-process) сервером. Именно о серверах этого типа пойдет речь.

Если же сервер автоматизации выполняется на компьютере, отличном от компьютера, на котором выполняется контроллер, он называется удаленным. Отметим, что многие локальные серверы автоматизации могут быть запущены как удаленные.

Если клиент и сервер находятся в разных адресных пространствах (неважно, на одном компьютере или на разных), то для управления сервером клиент должен обращаться к методам объектов, находящихся в другом адресном пространстве. Для этой цели используется технология LRPC (Local Remote Procedure Calls - локальные вызовы удаленных процедур).

Как было сказано ранее, каждый СОМ-сервер (каковым является сервер автоматизации) и каждый класс СОМ-объектов обладает уникальным 128-битовым идентификатором GIUD (Global Unique Identifier). При обращении к классам СОМ-объектов он иногда называется CLSID (идентификатор класса). При создании СОМ-серверов (в том числе и серверов автоматизации) с помощью C++Builder GUID и CLSID генерируются автоматически, хотя при необходимости можно сгенерировать их с помощью вызова стандартной функции Windows API CoCreateGUID. Информация обо всех СОМ-серверах и классах СОМ-объектов хранится в системном реестре, что позволяет клиенту "не знать", в каком каталоге (или на каком компьютере локальной сети) находится СОМ-сервер, а получать информацию о нем из реестра.

В общем случае СОМ-сервер представляет собой приложение, которое создает СОМ-объект и делает его доступным для других программ. Сервер автоматизации предоставляет своим клиентам для доступа объект специального типа - dispatch object. При этом в адресном пространстве приложения-контроллера, управляющего сервером, присутствует вариантная переменная, содержащая интерфейс IDispatch, предоставляющий ему доступ к этому объекту на СОМ-сервере.

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

Создание приложения, подлежащего автоматизации

Для создания сервера автоматизации следует создать обычное приложение и затем добавить к нему описание класса СОМ-объектов, создаваемых этим приложением, для осуществления доступа к функциональности, которую нужно автоматизировать.

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

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

void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
  Memo1->Lines->Clear ();
}
void __fastcall TForm1::SpeedButton2Click(TObject *Sender)
{
  if (OpenDialog1->Execute ())
  {
    Memo1->Lines->LoadFromFile (OpenDialog1->FileName);
  }
}
void __fastcall TForm1::SpeedButton3Click(TObject *Sender)
{
  if (SaveDialog1->Execute ())
  {
    Memo1->Lines->SaveToFile (OpenDialog1->FileName);
  }
}
void __fastcall TForm1::SpeedButton4Click(TObject *Sender)
{
  Close ();
}

Сохраним проект. Отметим, что пока созданный нами текстовый редактор представляет собой обычное Windows-приложение и не является сервером автоматизации.

Превращение приложения в сервер автоматизации

Для превращения созданного нами выше приложения в сервер автоматизации выберем элемент Automation Object со страницы ActiveX репозитария объектов.

Введем имя класса, под которым данный класс СОМ-объектов будет зарегистрирован в реестре.

После этого мы окажемся в редакторе библиотеки типов (Type Library Editor), в котором нам предстоит определить свойства и методы созданного класса СОМ-объектов.

Библиотека типов

По существу библиотека типов представляет собой двоичный файл с описанием интерфейсов СОМ-объекта и их методов.

Обычно такие описания создаются с помощью специального языка IDL (Interface Definition Language) и используются для того, чтобы разработчики знали, как создать код, реализующий методы СОМ-объекта (или вообще методы объекта, расположенного за пределами адресного пространства разрабатываемого приложения, так как IDL используется не только в СОМ-технологии, но и в иных технологиях, реализующих вызовы удаленных процедур или функций, например CORBA). Помимо этого, описания методов на языке IDL могут быть использованы для автоматической генерации серверного и клиентского кода (так называемого proxy-кода и stub-кода) с помощью соответствующих утилит.

С другой стороны, proxy-код и stub-код для клиента и сервера могут быть сгенерированы динамически. В этом случае клиент должен динамически получать информацию о свойствах и методах интерфейсов СОМ-объекта, и тогда наличие библиотеки типов, содержащей такую информацию, может быть весьма удобно. Отметим, что библиотеку типов можно в принципе сгенерировать на основе описания на языке IDL с помощью специального компилятора MIDL, но в данном случае в этом нет необходимости.

Итак, приступим к редактированию библиотеки типов. Предположим, что мы хотим автоматизировать загрузку файла в окно редактора, сохранение набранного текста, очистку окна редактирования, определение и изменение ширины и видимости окна. Создадим также метод, добавляющий строку к редактируемому тексту. С этой целью опишем для нашего сервера методы FileNew, FileOpen, FileSave, AddLine и их параметры, а также свойства Width и Visible.

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

short 2-байтовое целое число со знаком
long 4-байтовое целое число со знаком
single 4-байтовое действительное число
double 8-байтовое действительное число
BSTR двоичная строка
DATE дата
VARIANT_BOOL true = -1, false = 0
VARIANT Указатель на вариантную переменную

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

Метод NewFile параметров не имеет. Методы OpenFile и SaveFile имеют один строковый параметр типа BSTR - имя файла. Метод AddLine также имеет один строковый параметр, задающий добавляемую строку. Свойство Visible имеет логический тип VARIANT_BOOL, при этом оно может быть как прочитано, так и изменено. Свойство Width имеет целый тип long (число пикселов) и также доступно как для чтения, так и для записи.

Реализация методов объекта автоматизации

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

STDMETHODIMP TMyAutoImpl::AddLine(BSTR lpwString)
{
  Form1->Memo1->Lines->Add (lpwString);
}
STDMETHODIMP TMyAutoImpl::get_Visible(VARIANT_BOOL* Value)
{
  try
  {
    *Value = Form1->Visible;
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyAuto);
  }
  return S_OK;
};
STDMETHODIMP TMyAutoImpl::get_Width(long* Value)
{
  try
  {
    *Value = Form1->Width;
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyAuto);
  }
  return S_OK;
};
STDMETHODIMP TMyAutoImpl::NewFile()
{
  Form1->Memo1->Lines->Clear ();
}
STDMETHODIMP TMyAutoImpl::OpenFile(BSTR lpwFileName)
{
  Form1->Memo1->Lines->LoadFromFile (lpwFileName);
}
STDMETHODIMP TMyAutoImpl::SaveFile(BSTR lpwFileName)
{
  Form1->Memo1->Lines->SaveToFile (lpwFileName);
}
STDMETHODIMP TMyAutoImpl::set_Visible(VARIANT_BOOL Value)
{
  try
  {
    Form1->Visible = Value;
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyAuto);
  }
  return S_OK;
};
STDMETHODIMP TMyAutoImpl::set_Width(long Value)
{
  try
  {
    Form1->Width = Value;
  }
  catch(Exception &e)
  {
    return Error(e.Message.c_str(), IID_IMyAuto);
  }
  return S_OK;
};

Скомпилируем и запустим сервер на выполнение. При этом он зарегистрируется в реестре. В разделах реестра HKEY_CLASSES_ROOT и HKEY_LOCAL_MASHINE\SOFTWARE появятся несколько записей, связанных с данным сервером и его интерфейсами, в том числе и информация о местоположении сервера.

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

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

Создание контроллера

На главной форме будущего приложения-контроллера разместим 10 кнопок, а также компоненты TEdit, TCheckBox, TOpenDialog, TSaveDialog. Создадим обработчики событий, связанные с нажатием на кнопки (при этом следует подключить модуль ComObj.hpp):

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  serv = CreateOleObject ("Project1.MyAuto");
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
  serv = Unassigned;
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
  Edit1->Text = serv.OlePropertyGet ("Width");
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  serv.OlePropertySet ("Width", atoi (Edit1->Text.c_str ()));
}
void __fastcall TForm1::Button8Click(TObject *Sender)
{
  CheckBox1->Checked = serv.OlePropertyGet ("Visible");
}
void __fastcall TForm1::Button9Click(TObject *Sender)
{
  serv.OlePropertySet("Visible", CheckBox1->Checked);
}
void __fastcall TForm1::Button5Click(TObject *Sender)
{
  if (OpenDialog1->Execute ())
  {
    serv.OleProcedure ("OpenFile",
      WideString((OpenDialog1->FileName).c_str ()).c_bstr ());
  }
}
void __fastcall TForm1::Button6Click(TObject *Sender)
{
  if (SaveDialog1->Execute ())
  {
    serv.OleProcedure ("SaveFile",
      WideString((SaveDialog1->FileName).c_str ()).c_bstr ());
  }
}
void __fastcall TForm1::Button7Click(TObject *Sender)
{
  serv.OleProcedure ("NewFile");
}
void __fastcall TForm1::Button10Click(TObject *Sender)
{
  serv.OleProcedure ("AddLine",
    WideString((Edit1->Text).c_str ()).c_bstr ());
}

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

Для управления сервером автоматизации мы создали переменную типа Variant (в C++Builder для этой цели имеется соответствующий класс) и вызвали функцию CreateOleObject, содержащуюся в модуле ComObj.hpp библиотеки VCL.

При выполнении функции CreateOleObject произойдет следующее. Эта функция, вызвав несколько функций COM API, создаст экземпляр СОМ-объекта IDispatch и вернет его внутри вариантной переменной. Этот объект, в свою очередь, содержит интерфейс объекта (в данном случае нашего сервера автоматизации), методы которого мы хотим вызывать из приложения. Если исследовать реализацию функции CreateOleObject в исходном тексте модуля ComObj.hpp, можно обнаружить, что она, в свою очередь, вызывает функцию COM API CoCreateInstance, назначение которой - создать объект из исполняемого файла или DLL. Переменная типа Variant может содержать разнообразные данные (строку, число и др., в том числе и интерфейс СОМ-объекта).

Отметим, что в отличие от Visual Basic или Delphi C++Builder не позволяет обращаться к методам и свойствам вариантных переменных, существование которых заранее неизвестно. Поэтому допустимый в Delphi код вида

if VarType(Serv) = varDispatch then
Serv.Width := StrToInt(Edit1.Text);

не имеет аналога в C++Builder. Дело в том, что при создании контроллеров с помощью Delphi в случае объектов типа Variant в отличие от объектов другого типа, например TForm, компилятор не проверяет, имеется ли в действительности такое свойство (в данном случае Width) у данного объекта. На этапе выполнения такого кода происходит вызов функций Win32 API, в результате работы которых меняется свойство Width объекта, содержащегося не в адресном пространстве созданного контроллера, а в адресном пространстве сервера.

В С++Builder достичь такого же результата можно с помощью следующего кода:

if (VarType(Serv) == varDispatch)
{
  Serv.OlePropertySet ("Width", StrToInt(Edit1->Text));
}

В этом случае на этапе выполнения производится вызов тех же самых функций Win32 API, что и в предыдущем случае. OlePropertySet представляет собой оболочку для метода вариантной переменной Exec () (наряду с OlePropertyGet, OleProcedure и OleFunction, позволяющими получать значения свойств объектов автоматизации и выполнять их методы). Отметим, что в Delphi также можно использовать вызовы OlePropertySet, OlePropertyGet, OleProcedure, OleFunction.

После запуска контроллера при нажатии кнопки "Connect" запускается сервер. При нажатии кнопки "Disconnect" он выгружается. При нажатии кнопок "New File", Öpen File" и "Save File" происходит очистка окна редактирования, загрузка текста в окно редактирования сервера из файла, сохранение текста в файле. Кнопка "Get Visible" показывает и скрывает окно сервера в зависимости от наличия отметки возле надписи "Visible", при этом в невидимом состоянии сервер продолжает выполнять свои функции. Нажатие кнопки "Set Visible" приводит отметку возле надписи "Visible" в соответствие значению свойства "Visible" сервера. Нажатие кнопки "Get Width" приводит к тому, что в строке редактирования в верхней части окна контроллера отображается ширина окна сервера в пикселах. Если ввести в строку редактирования другое число и нажать кнопку "Set Width", ширина окна сервера станет равной введенному числу пикселов. Нажатие кнопки Ädd String" приводит к тому, что в редактируемый текст добавляется строка, находящаяся в этот момент в компоненте TEdit.

Раннее и позднее связывание

Рассмотренный выше пример создания контроллера использовал так называемое позднее связывание (late binding), или связывание на этапе выполнения. В этом случае, как было сказано выше, анализ существования методов и свойств объекта автоматизации не производится до момента обращения к ним на этапе выполнения. Поэтому при создании контроллера таким образом высока вероятность незамеченных ошибок в написании названий методов и свойств.

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

Чтобы иметь возможность создать такой контроллер, следует из меню C++Builder выбрать опцию "Project/Import Type Library". В результате будут созданы файлы в подкаталоге Imports. Эти файлы следует включить в проект контроллера, чтобы сделать доступными описания классов для управления сервером (C++Builder делает это автоматически).

В этом случае код контроллера можно создать двумя способами. Один их этих способов реализует так называемое раннее связывание (early binding) клиента с сервером, то есть создание и использование в клиентском приложении таблицы виртуальных методов, полностью аналогичной таблице виртуальных методов сервера.

Пример кода контроллера автоматизации, реализующего раннее связывание, приведен ниже:

IMyAuto *serv;

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  serv = CoMyAuto::Create ();
}
void __fastcall TForm1::Button2Click(TObject *Sender)
{
  serv = NULL;
}
void __fastcall TForm1::Button3Click(TObject *Sender)
{
  if (serv)
  {
    Edit1->Text = IntToStr (serv->Width);
  }
}
void __fastcall TForm1::Button4Click(TObject *Sender)
{
  if (serv)
  {
    serv->Width = atoi (Edit1->Text.c_str ());
  }
}
void __fastcall TForm1::Button8Click(TObject *Sender)
{
  if (serv)
  {
    CheckBox1->Checked = serv->Visible;
  }
}
void __fastcall TForm1::Button9Click(TObject *Sender)
{
  if (serv)
  {
    serv->Visible = CheckBox1->Checked;
  }
}
void __fastcall TForm1::Button5Click(TObject *Sender)
{
  if (serv)
    if (OpenDialog1->Execute ())
    {
      serv->OpenFile (WideString((OpenDialog1->FileName).c_str ()).c_bstr ());
    }
}
void __fastcall TForm1::Button6Click(TObject *Sender)
{
  if (serv)
    if (SaveDialog1->Execute ())
    {
      serv->SaveFile (WideString((SaveDialog1->FileName).c_str ()).c_bstr ());
    }
}
void __fastcall TForm1::Button7Click(TObject *Sender)
{
  if (serv)
  {
    serv->NewFile ();
  }
}
void __fastcall TForm1::Button10Click(TObject *Sender)
{
  if (serv)
  {
    serv->AddLine (WideString((Edit1->Text).c_str ()).c_bstr ());
  }
}

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

Есть еще и третий способ, основанный также на импорте библиотеки типов, но использующий позднее связывание:

IMyAuto3Disp V;
...
void __fastcall TForm1::Button1Click (TObject *Sender)
{
  V = (IMyAuto3Disp) CreateOleObject ("Autoserv.MyAuto3");
}

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

Хотелось бы обратить внимание на то, что, хотя мы и смогли протестировать свойства и методы сервера автоматизации с помощью созданного контроллера, у нас еще не было возможности произвести отладку части кода, связанного с автоматизацией. Естественно, если клиент запускается под управлением среды разработки C++Builder, использовать тот же самый экземпляр среды разработки для отладки сервера невозможно. Поэтому следует открыть проект сервера в отдельном экземпляре среды разработки и запустить его на выполнение. Если после этого запустить приложение-контроллер (неважно, под управлением другого экземпляра среды разработки или просто средствами операционной системы) и нажать кнопку "Connect", контроллер соединится с уже запущенным экземпляром сервера. Если в исходном тексте сервера отмечены точки останова, при их достижении выполнение кода сервера прекращается и управление передается среде разработки.

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

Создание контроллеров для произвольных серверов автоматизации

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

Каким образом можно получить информацию о них?

Обычно такие сведения содержатся в документации или файлах справочной системы, поставляемых с данным сервером, как, например, это сделано в MS Office или Seagate Crystal Reports Professional. Но в принципе такую информацию можно получить и из библиотеки типов.

В качестве примера рассмотрим использование информации из библиотеки типов MS Excel как одного из наиболее часто применяемых серверов автоматизации в практике отечественных разработчиков. Надо заметить, что практически все, что может сделать пользователь, работая с этим приложением, равно как и с другими приложениями MS Office, доступно для автоматизации.

Как и в предыдущем случае, для управления сервером автоматизации следует создать переменную типа Variant и вызвать функцию CreateOleObject:

Variant XL;
...
XL = CreateOleObject("Excel.Application.9");

В качестве параметра функции CreateOleObject передается имя объекта, который мы хотим создать. Найти его можно в реестре Windows.

Коллекции объектов внутри серверов автоматизации

Внутри некоторых OLE-серверов (в частности, приложений MS Office) существует иерархия вложенных объектов примерно следующего вида.

Свойствами объектов Excel могут являться так называемые коллекции объектов. Например, коллекция Workbooks является свойством объекта Excel.Application, при этом она содержит набор вложенных объектов — рабочих книг Excel, а те, в свою очередь, обладают свойством Worksheets, представляющим собой коллекцию рабочих листов, каждый из которых обладает свойством Cells, являющимся коллекцией ячеек. Аналогично коллекция Charts также является свойством рабочей книги, и, соответственно, внутри свойствами объектов Word могут быть коллекции Paragraphs, Words, Tables.

В С++Builder обращение к члену коллекции производится следующим образом:

Variant  MyWorkbook = XL.OlePropertyGet ("WorkBooks")
                        .OlePropertyGet ("Item", 1);

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

Код, заставляющий Excel выполнить эти действия, будет выглядеть следующим образом:

Variant XL, v0, v1, v2;

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  XL = CreateOleObject ("Excel.Application.9");
  XL.OlePropertySet ("Visible", true);
  v0 = XL.OlePropertyGet ("Workbooks");
  v0.OleProcedure ("Add");
  v1 = v0.OlePropertyGet ("Item", 1);
  v0 = v1.OlePropertyGet ("Worksheets") ;
  v0.OlePropertyGet ("Item",1)
    .OlePropertySet ("Name", "Бухгалтерия желтая");
  v0.OlePropertyGet ("Item", 2)
    .OlePropertySet ("Name", "Бухгалтерия красная");
  for (int j = 1; j < 3; j++)
  {
    v1 = v0.OlePropertyGet ("Item", j);
    for (int i = 1; i < 11; i++)
    {
      v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 1)
        .OlePropertySet ("Value", i);
      v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 2)
        .OlePropertySet ("Value", i * 5);
      v2 = v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 2);
      v2.OlePropertyGet ("Font").OlePropertySet ("Color", clBlue);
      v2.OlePropertyGet ("Font").OlePropertySet ("Bold", true);
      v2.OlePropertyGet ("Interior")
        .OlePropertySet ("ColorIndex", 9 - 3 * j);
    }
    v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", 11, 1)
      .OlePropertySet ("Value", "=SUM(A1:A10)");
    v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", 11, 2)
      .OlePropertySet ("Value", "=SUM(B1:B10)");
  }
  // отключить диагностику при закрытии сервера
  XL.OlePropertySet ("DisplayAlerts", false);
  XL.OlePropertyGet ("Workbooks").OlePropertyGet ("Item", 1)
    .OleProcedure ("SaveAs", "test.xls");
  XL.OleProcedure ("Quit");
}

Отметим, что для запуска Excel в фоновом режиме без отображения его окна на экране достаточно убрать из кода С++Builder строку:

XL.OlePropertySet ("Visible", true);

Следует обратить внимание на то, что вариантная переменная XL объявлена за пределами процедуры манипуляции OLE-сервером. Это сделано для того, чтобы существование вариантной переменной не ограничивалось данной процедурой.

Использование информации из библиотек типов

Отметим, что в приведенном выше примере рабочая книга и рабочие листы были созданы с применением параметров, предлагаемых по умолчанию. Каким образом можно изменить эти параметры?

Рассмотрим следующий пример кода:

XL = CreateOleObject ("Excel.Application.9");
XL.OlePropertySet ("Visible", true);
v0 = XL.OlePropertyGet ("Workbooks");
v0.OleProcedure ("Add", -4109);

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

Откуда в данном случае взялась константа -4109? Каким образом можно узнать, какими еще константами можно пользоваться при автоматизации с использованием данного сервера?

Ответы на эти вопросы можно получить, открыв библиотеку типов данного сервера. Сделать это можно, выбрав пункт меню "File/Open", указав в списке возможных файлов Type Library и выбрав соответствующую библиотеку типов (для Excel это Excel8.olb, для Word - MSWord8.olb). При этом по истечении некоторого времени (эти библиотеки очень велики) будут созданы файлы с описанием всех использованных констант, а также свойств и методов содержащихся в сервере объектов (в данном случае Excel_TLB.cpp и Excel_TLB.h для С++Builder либо Excel_TLB.pas для Delphi). При создании этих файлов появятся сообщения об ошибках, связанные с тем, что в коде Excel и Word могут использоваться зарезервированные слова Object Pascal или С++.

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

Именно там и содержится ссылка на константу xlWBATChart = -4109, соответствующую шаблону диаграммы. При необходимости использовать в коде приложения-клиента именованные константы можно сослаться на файл Excel_TLB.h или Excel_TLB.pas в тексте модуля приложения. Точно так же можно определить, каковы свойства и методы вложенных объектов данного сервера автоматизации:

Используя сведения о константах, объектах, свойствах и методах OLE-сервера, можно модифицировать приведенный выше пример. Теперь мы создадим в Excel график на основе данных из рабочих листов, скопируем его в буфер обмена и перенесем в документ Word (об объектах и константах которого узнаем из библиотеки типов Word), снабдив график подписью. Соответствующий код выглядит следующим образом:

Variant XL, v0, v1, v2, v22, vrange, WD, a, b, c;

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  XL = CreateOleObject ("Excel.Application.9");
  XL.OlePropertySet ("Visible", true);
  v0 = XL.OlePropertyGet ("Workbooks");
  v0.OleProcedure ("Add");
  v1 = v0.OlePropertyGet ("Item", 1);
  v0 = v1.OlePropertyGet ("Worksheets");
  v22 = v1.OlePropertyGet ("Charts");
  v22.OleProcedure ("Add");
  v0.OlePropertyGet ("Item", 1)
    .OlePropertySet ("Name", "Бухгалтерия желтая");
  v0.OlePropertyGet ("Item", 2)
    .OlePropertySet ("Name", "Бухгалтерия красная");
  for (int j = 1; j < 3; j++)
  {
    v1 = v0.OlePropertyGet ("Item", j);
    for (int i = 1; i < 11; i++)
    {
      v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 1)
        .OlePropertySet ("Value", i);
      v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 2)
        .OlePropertySet ("Value", i * 5);
      v2 = v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", i, 2);
      v2.OlePropertyGet ("Font").OlePropertySet ("Color", clBlue);
      v2.OlePropertyGet ("Font").OlePropertySet ("Bold", true);
      v2.OlePropertyGet ("Interior")
        .OlePropertySet ("ColorIndex", 9 - 3 * j);
    }
    v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", 11, 1)
      .OlePropertySet ("Value", "=SUM(A1:A10)");
    v1.OlePropertyGet ("Cells").OlePropertyGet ("Item", 11, 2)
      .OlePropertySet ("Value", "=SUM(B1:B10)");
  }
  vrange = v0.OlePropertyGet ("Item", 1)
             .OlePropertyGet ("Range", "A1:A10");
  v1 = v22.OlePropertyGet ("Item", 1);
  v2 = v1.OlePropertyGet ("SeriesCollection");
  v2.OleProcedure ("Add", vrange);
  vrange = v0.OlePropertyGet ("Item", 1)
             .OlePropertyGet ("Range", "B1:B10");
  v2.OleProcedure ("Add", vrange);
  v1.OleProcedure ("Select");
  XL.OlePropertyGet ("Selection").OleProcedure("Copy");
  WD = CreateOleObject ("Word.Application.9");
  WD.OlePropertySet ("Visible", true);
  WD.OlePropertyGet ("Documents").OleProcedure ("Add");
  a = WD.OlePropertyGet ("Documents");
  b = a.OleFunction ("Item", 1);
  for (int i = 1; i < 5; i++)
  {
    b.OlePropertyGet ("Paragraphs").OleProcedure("Add");
  }
  c = b.OleFunction ("Range", 1, 2);
  c.OleProcedure ("Paste");
  c = b.OleFunction ("Range", 3, 3);
  c.OlePropertySet ("Text", "График, скопированный из рабочей книги Excel");
  XL.OlePropertySet ("DisplayAlerts", false);
  XL.OlePropertyGet ("Workbooks").OlePropertyGet ("Item", 1)
    .OleProcedure ("SaveAs", "test.xls");
  XL.OleProcedure ("Quit");
  WD.OlePropertySet ("DisplayAlerts", false);
  b.OleProcedure ("SaveAs", "test2.doc");
  WD.OleProcedure ("Quit");
}

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

Создание коллекций объектов

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

Если вернуться к простейшему примеру, рассмотренному ранее, мы можем обнаружить в нем подходящий объект для создания коллекции - строки, содержащиеся в окне текстового редактора. Модифицируем его, добавив свойства Lines (коллекция строк) и LineCount (число строк), методы для их считывания и метод для модификации строки с указанным номером.

Код реализации вновь введенных методов для считывания вновь введенных свойств Lines и LineCount и модификации свойства Lines будет выглядеть следующим образом:

STDMETHODIMP TMyAutoImpl::get_LineCount (int *Value)
{
  *Value = Form1->Memo1->Lines->Count;
}
STDMETHODIMP TMyAutoImpl::get_Lines (int Index, BSTR *Value)
{
  if (Index >= 0 && Index <= Form1->Memo1->Lines->Count)
  {
    *Value = Form1->Memo1->Lines[Index];
  }
  else
  {
    *Value = "";
  }
}
STDMETHODIMP TMyAutoImpl::set_Lines (int Index, BSTR Value)
{
  if (Index >= 0 && Index <= Form1->Memo1->Lines->Count)
  {
    Form1->Memo1->Lines[Index] = Value;
  }
}

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

Обработчики событий, связанные с нажатием на кнопки "Get Line Count", "Get Line", "Set Line", для C++Builder выглядят следующим образом:

void __fastcall TForm1::Button11Click (TObject *Sender)
{
  Edit2->Text = IntToStr (serv.LineCount);
}
void __fastcall TForm1::Button12Click (TObject *Sender)
{
  serv.Lines[StrTolnt(Edit2->Text)-1] = Edit1->Text;
}
void __fastcall TForm1::Button13Click (TObject *Sender)
{
  Edit1->Text = serv.Lines[StrTolnt(Edit2->Text)-1];
}

Конструкция serv.Lines[StrTolnt(Edit2->Text)-1]; использована в приведенном коде потому, что элементы массива, реализующего коллекцию, нумеруются не с единицы, а с нуля, тогда как строки на экране более привычно начинать нумеровать с единицы.

Экспонируемые свойства и методы

В принципе сервер автоматизации может содержать любые свойства и методы. Однако существует спецификация Microsoft на серверы автоматизации, которой следует руководствоваться при их создании. Ниже приведены список методов и свойств, которые согласно этой спецификации, должны быть реализованы. Следует обратить внимание на то, что здесь приведены два списка - один для сервера-приложения (табл. ), а другой - для сервера-документа (табл. ). В случае SDI-приложений (Single Document Interface - однодокументный интерфейс) они могут совпадать. Но в MDI-приложениях (Multiple Document Interface - многодокументный интерфейс) необходимо создавать отдельный сервер для документа. При этом он не должен регистрироваться в системном реестре, так как документ невозможно показать без приложения.

Свойства и методы объекта автоматизации приложения

Имя Тип/InOut Обязатель- Тип данных Описание
ность(+)
Желатель-
ность(-)
ActiveDocument Свойство/- - IDispatch, Активный документ
Unassigned
Application Свойство/- + IDispatch Приложение
Caption Свойство/+ - BSTR Заголовок
приложения
DefaultFilePath Свойство/+ - BSTR Путь по умолчанию.
Используется для
открытия/создания
файлов по умолчанию
Documents Свойство/- - IDispatch, Коллекция
коллекция документов
FullName Свойство/- + BSTR Путь/имя приложения
Height Свойство/+ - float Высота главной формы
(режим MM_HIMETRIC)
Help Метод - Показывает справку
Interactive Свойство/+ - VARIANT_BOOL Изменение документов
пользователем
Left Свойство/+ - float Координата левого
верхнего угла
(режим MM_HIMETRIC)
Name Свойство/- + BSTR Краткое описание
приложения
Parent Свойство/- + IDispatch = Application
Path Свойство/- - BSTR Путь к приложению
Quit Метод + Закрытие приложения
Repeat Метод - Повторяет последнюю
команду пользователя
StatusBar Свойство/+ - BSTR Содержимое
панели состояния
Top Свойство/+ - float Координата левого
верхнего угла
(режим MM_HIMETRIC)
Undo Метод - Отменяет последнюю
команду пользователя
Visible Свойство/+ + VARIANT_BOOL Видимость приложения
Width Свойство/+ - float Ширина главной формы
(режим MM_HIMETRIC)

Свойства и методы объекта автоматизации документа из коллекции

Имя Тип/InOut Обязатель- Тип данных Описание
ность(+)
Желатель-
ность(-)
Activate Метод + Делает документ активным
Application Свойство/- + IDispatch Объект приложения
Author Свойство/+ - BSTR Имя автора
Close Метод + Закрывает документ
Comments Свойство/+ - BSTR Комментарии к документу
FullName Свойство/- + BSTR Путь и имя файла
с документами
Keywords Свойство/+ - BSTR Ключевые слова
Name Свойство/- + BSTR Имя документа
NewWindow Метод - Добавляет в документ
новое окно
Parent Свойство/- + IDispatch Родитель документа
Path Свойство/- + BSTR Путь к файлу документа
Print Метод + Печать документа
PrintOut Метод - = Print
PrintPreview Метод - Показ образца печати
ReadOnly Свойство/+ - VARIANT_BOOL Возможность редактирования
RevertToSave Метод - Откат всех изменений
Save Метод + Сохранение документа
SaveAs Метод + Сохранение документа
под другим именем
Saved Свойство/- + VARIANT_BOOL документ не изменялся
с момента сохранения
Subject Свойство/+ - BSTR Сводка документа
Title Свойство/+ - BSTR Заголовок документа

В первой колонке таблицы указано название метода или свойства, под которым данный метод или свойство должны экспонироваться. Во второй колонке указаны тип - метод или свойство - и то, является ли свойство свойством "только для чтения" (-) или позволяет как чтение, так и запись (+). В третьей колонке указано, является ли метод/свойство обязательным для экспонирования (+) или желательным (-). В четвертой колонке указан тип данных для свойств. И наконец, в пятой колонке приведено краткое описание.

Можно заметить некоторое дублирование - например, имеются одинаковые методы Print и PrintOut. Это связано с тем, что в Visual Basic есть внутренний метод Print и его использование может привести к путанице.

Предыдущая Оглавление Следующая