Создание контроллеров автоматизации с помощью C++Builder

Наталия Елманова

    Тестирование сервера автоматизации
      Создание контроллера
      Некоторые комментарии
    Создание контроллеров для произвольных серверов автоматизации
      Коллекции объектов внутри серверов автоматизации
      Использование информации из библиотек типов

Тестирование сервера автоматизации

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

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

На главной форме будущего приложения-контроллера разместим компоненты TEdit, TCheckBox, TOpenDialog, TSaveDialog, а также десять кнопок.

Главная форма контроллера автоматизации

Рис. 1. Главная форма контроллера автоматизации

Создадим обработчики событий, связанные с нажатием на кнопки (при этом следует сослаться на h-файл модуля ComObj):

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <ComObj.hpp>
#include "autocon.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm2 *Form2;
Variant Serv;
//---------------------------------------------------------------------------
__fastcall TForm2::TForm2(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button3Click(TObject *Sender)
{
Serv=CreateOleObject("Project1.MyAuto3");
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button1Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch)
Edit1->Text=IntToStr(Serv.OlePropertyGet("Width"));
// имеет смысл проверить, что именно лежит в вариантной переменной…
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button2Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch) Serv.OlePropertySet("Width",StrToInt(Edit1->Text));
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button12Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch)
CheckBox1->Checked=Serv.OlePropertyGet("Visible");
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button13Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch) Serv.OlePropertySet("Visible",CheckBox1->Checked);
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button11Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch) Serv=Unassigned;
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button5Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch)
{if (OpenDialog1->Execute())
Serv.OleProcedure("OpenFile",OpenDialog1->FileName);}
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button6Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch)
{ if (SaveDialog1->Execute())
Serv.OleProcedure("SaveFile",SaveDialog1->FileName);}
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button7Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch) Serv.OleProcedure("NewFile");
}
//---------------------------------------------------------------------------
void __fastcall TForm2::Button4Click(TObject *Sender)
{
if (VarType(Serv)==varDispatch)
Serv.OleProcedure("AddLine",Edit1->Text);
}
//---------------------------------------------------------------------------

Некоторые комментарии

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

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

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

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

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

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

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

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

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

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

Контроллер и сервер автоматизации, запущенные одновременно

Рис. 2. Контроллер и сервер автоматизации, запущенные одновременно

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

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

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

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

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

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

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

Variant XL;
……
XL=CreateOleObject("Excel.Application.8");

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

Запись в реестре, соответствующая выбранному серверу

Рис. 3. Запись в реестре, соответствующая выбранному серверу.

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

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

Примерная иерархия вложенных объектов OLE-сервера

Рис. 4. Примерная иерархия вложенных объектов OLE-сервера.

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

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

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

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

  • создание в Excel рабочей книги с двумя страницами и какими-нибудь именами;
  • заполнение первых 10 ячеек двух первых колонок числами;
  • вычисление в одиннадцатой ячейке их суммы;
  • изменение цвета и начертания шрифта в одной из колонок, а также цвета ячеек;
  • сохранение полученной рабочей книги в файле.

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

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <ComObj.hpp>
#include "xlauto2.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Variant XL,v0,v1,v2;
//Function Item("Item");
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
XL=CreateOleObject("Excel.Application.8");
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);

Результат работы приложения

Рис. 5. Результат работы приложения

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

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

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

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

XL=CreateOleObject("Excel.Application.8");
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:

Значения констант, описывающих шаблоны MS Excel 97

Рис. 6. Значения констант, описывающих шаблоны MS Excel 97

Именно там и содержится ссылка на константу xlWBATChart = -4109, соответствующую шаблону диаграммы.

При необходимости использовать в коде приложения-клиента именованные константы можно сослаться на файл Excel_TLB.h иди Excel_TLB.pas в тексте модуля приложения.

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

Свойства и методы коллекции Workbooks

Рис. 7. Свойства и методы коллекции Workbooks

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

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "ole2.h"
#include <ComObj.hpp>
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
Variant XL,v0,v1,v2, v22, vrange, WD,a,b,c;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
XL=CreateOleObject("Excel.Application.8");
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.8");
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); //ioee??eou aeaaiinoeeo
XL.OlePropertyGet("Workbooks").OlePropertyGet("Item",1).OleProcedure("SaveAs","test.xls");
XL.OleProcedure("Quit");
WD.OlePropertySet("DisplayAlerts",false); //ioee??eou aeaaiinoeeo
b.OleProcedure("SaveAs","test2.DOC");
WD.OleProcedure("Quit");
}
//---------------------------------------------------------------------------

Результаты работы приложения представлены на рис. 8.

Результат автоматизации создания диаграммы Excel и переноса ее в Word

Рис.8. Результат автоматизации создания диаграммы Excel и переноса ее в Word

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

Следующая статья данного цикла будет посвящена использованию OLE-документов в приложениях C++Builder.

Координаты автора:
Центр Информационных Технологий,
Тел. (095)932-92-12, 932-92-13,
http://www.citmgu.ru/,
http://www.citforum.ru/