Доступ к COM серверам Microsoft Office из Delphi 5 (Статья)

(С) И. Мирончик, независимый разработчик и преподаватель курсов по Delphi, C++Builder, Oracle.
 E-maile EKSKHQ@Elsite.ru
 Эта статья была опубликована на сайте http://www.borland.ru/
== Введение ==
В статье рассматривается вопрос доступа к общеизвестным приложениям Microsoft Office, таким как Word, Excel, Outlook и другим, через новый набор компонент, представленных в Delphi 5. Для начала работы нам предстоит установить на компьютере приложения Microsoft Office 97 - Excel, Word, Outlook, PowerPoint. Если считаете необходимым, то можно добавить и Access (но с ним у меня особые счеты). Ну и конечно устанавливается новый продукт Delphi 5. Кроме множества изменений в нем имеется одно, для нас сейчас необходимое - новая закладка на палитре инструментов - Servers. Через эти компоненты мы будем получать доступ к COM серверам приложений Office, использующих автоматизацию (прежде известную как OLE Automation).
Мы рассмотрим несколько примеров построения контроллеров автоматизации для создания отчетов в MS Word, производство расчетов и построение диаграмм в MS Excel, а так же формирование рассылки писем адресатам через MS Outlook.
== Приложения и объекты MS Office. Обзор ==
Office - это среда, в которой большинство задач можно решать без какого-либо программирования. Но вся ценность приложений Office для разработчика заключается в том, что все, что можно сделать руками, можно сделать программным путем с использованием средств VBA (Visual Basic for Application). Кроме того, приложения Office поставляют сервера COM, которые предоставляют интерфейс доступа к приложению и его объектам. Благодаря этому, разработчик в среде Delphi имеет возможность, создав контроллер автоматизации, управлять сервером. Так как устроено приложение в Office? На самом деле приложение рассматривается как совокупность объектов со своими методами, свойствами, событиями, которые обеспечивают скелет приложения. Программист Office является не создателем приложения, как, например это делается в Delphi, а он принимает участие в создании системы документов. Таким образом, ДОКУМЕНТ, а не программа являются целью разработки.
Наследование - мощный инструмент построения нового класса, однако программистам известен еще один способ получения класса - встраивание. Как и наследование, встраивание транзитивное отношение. В объектной модели Office нет наследования в полном смысле этого слова, а есть только встраивание.
Всегда существует задающий приложение корневой объект, он всегда называется Application. Каждое приложение Office имеет свой собственный корневой объект - Word.Application, Excel.Application. Outlook приложение само является корневым объектом, несмотря на это в объект Application встраиваются все остальные объекты (участники), которые являются свойствами главного объекта. Участником могут быть свои участники и так далее. В документе методов очень много, причем самых разных, но имеются и одинаковые методы в различных приложениях Office. Среди таковых - Run, Quit, Activate; но даже и они в разных приложениях имеют различный набор параметров, а зачастую выполняют неадекватные действия. Такое многообразие различных реализаций классов отпугивало программистов контроллеров автоматизации. Однако, используя компоненты доступа к серверам автоматизации - положение резко улучшилось. На следующем рисунке показан очень маленький фрагмент структуры объекта Microsoft Word Objects. Более полное представление об объектной архитектуре приложений Office можно найти в соответствующих файлах помощи.
Как только открывается новый документ, будь то PowerPoint, Excel, Word, автоматически создается каркас нового документа, который представляет собой набор библиотек с классами. Объекты этих классов будут доступны в данном документе. Задачей разработчика контроллера автоматизации является получить доступ к корневому объекту сервера, выстроить цепочку доступа к объектам - участникам (встроенным объектам), правильно передать параметры.
== Объекты Application и организация доступа к ним из Delphi. ==
Я, наверное, утомил читателя устройством приложений Office. Но все-таки необходимо еще раз отметить, что всех объектов этого семейства не перечесть, но главные необходимо знать и уметь ими пользоваться. Фундаментальным объектом любого приложения является Application. Давайте получим к нему доступ из Delphi.
* Создаем новый проект
* На главную форму выкладываем компоненту с закладки Servers, которая называется WordApplication
* Устанавливаем свойства компоненты AutoConnect и AutoQuit в True
* Запускаем приложение на выполнение
Казалось бы, ничего не произошло, запустилась форма и отобразилась на экране, на самом деле наше приложение запустило сервер автоматизации Microsoft Word, этот факт можно обнаружить, запустив на выполнение Task Manager и выбрав закладку Processes. Среди прочих процессов мы обнаруживаем WINWORD.EXE. На самом деле была приложением проделана следующая работа:
:1. При создании формы, в системном реестре, по идентификатору CLSID был найден сервер Word.Application
:2. Запущено на выполнение приложение, находящееся по адресу в реестре (ProgID)
:3. Сервер предоставил нашему приложению, которое и является контроллером автоматизации интерфейс, через который мы и получим доступ к объекту Application.
:4. Интерфейс Idispatch унаследован от Iunknown, который в свою очередь имеет три метода, один из которых _ADDRef умеет считать количество клиентов, в настоящий момент использующих сервер. Как только от сервера отсоединиться последний клиент, он автоматически будет выгружен из памяти компьютера.
Закройте наше приложение и загляните в Task Manager - Word выгрузился из памяти.
Точно такой же результат можно было бы получить, прописав следующий код:
procedure TForm1.Button1Click(Sender: TObject);
var
 wd: OleVariant;
 fileName: string;
begin
 try
 fileName := ExtractFilePath(Application.EXEName) + 'report.DOC';
 // Создаем объект интерфейса для доступа к серверу COM
 wd := CreateOleObject('Word.Application');
 // Проверка наличия методов и правильность передачи параметров будет
 // осуществляться на стадии выполнения приложения
 wd.Application.Documents.Add;
 wd.Application.ActiveDocument.Range.InsertAfter(Now);
 wd.Application.ActiveDocument.SaveAs(fileName);
 // выгружаем сервер из памяти компьютера
 wd.Application.Quit(True, 0);
{...}
Не забудьте добавить в раздел uses модуль ComObj.
== Базовый класс TOLEServer ==
На закладке Service находится набор компонент для доступа к серверам автоматизации, не все компоненты возвращают ссылку на объект Application, то есть могут быть получены интерфейсы для доступа к вложенным объектам, таким как Документ Word или рабочая книга Excel. Все компоненты унаследованы от класса TOLEServer, который наследует свойства класса Tcomponent. TOLEServer является базовым классом всех COM серверов, которые можно получить через среду IDE следующим образом: Project | Import Type Library. Кроме этого этот класс имеет еще несколько свойств и методов для управления связью с COM сервером. Среди таковых уже знакомое нам свойство AutoConnect, которое автоматически запускает COM сервер и производит извлечение из него интерфейса, обеспечивающего связь с контроллером. Еще одно важное свойство класса TOLEServer - ConnectKind - указывающее тип процесса, к которому устанавливается связь. Свойство используется методом Connect, который вызывается автоматически, если свойство AutoConnect истинно. Ниже описаны значения, которые может принимать ConnectKind:
Значение свойства ConnectKind	Характеристика
ckRunningOrNew 	Контроллер производит подключение к уже существующему процессу, или запускает новый процесс, при отсутствии такового. Этот вид взаимодействия между COM сервером и контроллером наиболее часто применяется на практике. Такое значение свойства установлено по умолчанию.
ckNewInstance 	При соединении с сервером каждый раз создается новый экземпляр
ckRunningInstance 	Соединение устанавливается с уже запущенным COM сервером. Если таковой отсутствует - будет создан соответствующий объект ошибки, который необходимо обработать
ckRemote 	Это значение используется совместно со свойством RemoteMachineName, если необходимо подключиться к серверу на удаленной машине
ckAttachToInterface 	При установке этого значения интерфейс не создается и соответственно нельзя указывать значение True для свойства AutoConnect. Соединение с сервером производится с помощью метода ConnectTo
На последнем значении свойства ConnectKind равном ckAttachToInterface, хотелось бы остановиться более подробно. Действительно мы соединяемся с сервером через главный интерфейс, представленный в объекте Application. Но, например, возникает необходимость подключить к нашему проекту такие компоненты, как WordDocument или WordParagraphFormat, в этом случае мы просто производим подключение к уже существующему интерфейсу, а не создаем его заново. Также это может быть необходимо, когда контроллер должен отслеживать события, происходящие в COM сервере. Я предлагаю вам провести маленький эксперимент:
* Создайте новое приложение
* Выложите на форму компонент WordApplication и WordDocument
* Свойства AutoConnect и AutoQuit для WordApplication установите в True
* Свойство ConnectKind для WordDocument установите в ckAttachToInterface
* Для события onDokumentChange и onFormCreate пропишите следующий код:
procedure TForm1.WordApplication1DocumentChange(Sender: TObject);
begin
 // производим подключение к текущему документу
 WordDocument1.ConnectTo(WordApplication1.ActiveDocument);
 // Контроллер добавляет новую строку в текущий документ
 WordDocument1.Range.InsertAfter(#13 + 'Переход к документу' + #13
 + WordApplication1.ActiveDocument.Get_FullName + ' произведен :'
 + DateTimeToStr(Now));
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
 // COM сервер отображает себя на экране
 WordApplication1.Visible := True;
end;
После запуска приложения будет автоматически загружен Word, создайте в нем несколько новых документов и переключайтесь между ними с помощью меню Window. Вы увидите, что контроллер автоматизации добавляет новые строки в текущий активный документ. Аналогично можно управлять и сервером ExcelApplication. При создании новой рабочей книги на сервере, в контроллере будет проинициализировано событие onNewWorkBook, которое можно обработать аналогично предыдущему примеру.
== Классы - наследники TOleObject ==
Теперь давайте заглянем во внутренний мир компонент закладки Servers. Как мы уже убедились, все классы унаследованы от TOleObject. Кроме того, еще наследуется и интерфейс из библиотеки TLB. Как это происходит? Проведем следующее упражнение:
* Из среды программирования Delphi удаляем пакет с компонентами COM серверов (Component | Install Packages | Borland Sample Automation Servers Component | Remove). После этого закладка Servers удалилась.
* Создаем новый пакет с использованием библиотеки типов
** Выберите Project | Type library
** Из списка зарегистрированных серверов выберите библиотеку типов Excel (Microsoft Excel 8.0 Object Library)
** Укажите имя закладки палитры компонент (Pallete Page), куда будут установлены новые классы - TexcelQueryTable, TexcelApplication, TexcelChart, TexcelWorksheet, TexcelWorkbook, TExcelOLEObject. Выберите закладку Servers.
** В боксе - General Component Wraper установите флажок для генерации компоненты на основе библиотеки типов и размещении ее на палитре компонент.
** Нажмите кнопку Install
** Укажите имя пакета, где будет сгенерирован новый класс
** Установите сервер на палитру компонент.
После проделанных манипуляций вам стал доступен COM сервер Excel, в состав которого входят шесть классов. Аналогичным образом восстановите компоненты для сервера Word и Outlook.
Теперь мы можем посмотреть на объявление класса TWordApplication и его предка - TOleServer:
 TOleServer = class(TComponent, IUnknown);
 TWordApplication = class(TOleServer);
Благодаря такому объявлению, класс TWordApplication наследует свойства и методы класса TComponent (способен устанавливаться на палитре компонент и прочие..), а так же знает все о доступе к интерфейсам COM серверов, благодаря наследованию интерфейса IUnknown. В библиотеке типов прописаны все доступные методы и свойства COM сервера. Когда создается контроллер автоматизации (выкладываем на форму соответствующий компонент из палитры), то приложение получает доступ к Dual Interface описанный в библиотеке типов. Dual интерфейс - есть совокупность пользовательского интерфейса, описанного в библиотеке типов и dispinterface, который доступен в момент выполнения приложения. Это реализовано с помощью COM VTABLE интерфейса, унаследованного от IDispatch. Использование VTable интерфейса имеет ряд преимуществ:
* Передаваемые параметры, их типы и количество проверяется на стадии проектирования и редактор кода сопровождает разработчика всевозможными подсказками.
* Через VTable интерфейс осуществляется значительно более быстрый доступ к серверу автоматизации, чем через DispInterface
* В то же время, не всегда разработчик получает доступ к библиотеке типов и соответственно к V - таблице, поэтому приходится иногда пользоваться IDispatch интерфейсом
Ниже приведен пример использования Dual интерфейса
procedure TForm1.FormCreate(Sender: TObject);
var
 foo: TWordApplication;
 foo1: _Application;
begin
 foo := WordApplication1;
 foo1 := CoWordApplication.Create;
 ShowMessage(foo.UserName + #13 + foo1.Get_Name);
end;
Пример использования интерфейса с поздним связыванием был показан в начале этой статьи, когда доступ к COM серверу был осуществлен с помощью функции CreateOleObject. Компилятор в этом случае ничего не знает о методах и параметрах сервера, информация о них извлекается на стадии выполнения приложения, отсюда и потеря скорости выполнения приложения и всевозможные ошибки, которые компилятор не в состоянии обработать. При такой разработке приложения программист достает SDK от Microsoft Office и начинает старательно изучать большие тома литературы.
Подводя итог можно говорить о том, что для доступа к COM серверу автоматизации существует три способа
* VTable
* IDispatch
* Позднее связывание (CreateOleObject)
Наиболее прогрессивный - первый способ, через который работают компоненты Delphi 5 для доступа к COM серверам приложений Office.
== Пример использования VTable интерфейса ==
Постановка задачи: Имеется шаблон документа - shablon.DOC, подготовленный в MS Word, поля, которые должны быть заменены, помечены символом @. Необходимо прочитать данные из источника информации и заменить метки, на реальные данные, после чего распечатать полученный документ.
Воспользуемся методом VTable . Выложим на форму компоненту WordApplication, WordDocument и кнопку Button. Для события OnClick компоненты Button1 пропишем следующий код:
procedure TForm1.Button1Click(Sender: TObject);
var
 // Объявление переменных, для передачи их в
 // качестве формальных параметров в сервер автоматизации
 Shablon, FileName, oldStr, newStr, replace, ext: OleVariant;
begin
 Table1.Active := False;
 Table1.Active := True;
 Shablon := ExtractFilePath(Application.EXEName) + 'shablon.DOC';
 FileName := ExtractFilePath(Application.EXEName) + 'report.DOC';
 // Открываем шаблон документа
 WordApplication1.Documents.Open(Shablon, EmptyParam, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam);
 // Связываем компоненту с существующим интерфейсом
 WordDocument1.ConnectKind := ckAttachToInterface;
 WordDocument1.ConnectTo(WordApplication1.ActiveDocument);
 // Следующие переменные понадобятся нам для выполнения методов сервера
 Replace := 1;
 oldStr := '@1';
 newStr := DateTimeToStr(Now);
 // Находим в документе метки и производим их замены
 WordDocument1.Range.Find.Execute(oldStr, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, newStr, Replace);
 oldStr := '@2';
 newStr := WordApplication1.UserName;
 WordDocument1.Range.Find.Execute(oldStr, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, newStr, replace);
 {...}
 // сохранение документа и отображение его в OLE контейнере
 // (предварительный просмотр)
 WordDocument1.SaveAs(FileName);
 WordDocument1.Close;
 OleContainer1.CreateLinkToFile(FileName, False);
 OleContainer1.Refresh;
end;
Исходный код этого примера имеется в приложении к данной статье. Обратите внимание на передачу параметров в методы. Все параметры описаны как OleVariant и передаются в методы по ссылке (такая передача параметров характерна лишь для сервера Word, Excel и OutLook, например, принимают в основном фактические параметры). Сервер автоматизации сам будет разбираться, какой тип вы фактически подставили в качестве значения переменной, поэтому необходимо при разработке приложения особое внимание уделять хинтам, и почаще заглядывать в библиотеку типов сервера, с которым вы работаете в настоящее время. Кстати, после того как мы установили компоненты, библиотеки типов разместились в директории Delphi5\Imports\. В файлах библиотек типов можно найти, например все константы, необходимые для передачи параметров в Excel и OutLook, названия их интуитивно понятны. В модуле System - Delphi 5 описана переменная EmptyParam, которую необходимо использовать для передачи "пустышек" в качестве параметров.
var
 EmptyParam: OleVariant; { "Пустой параметр" который должен опционально использоваться в DUAL интерфейсе. }
Описание методов в библиотеке типов заставляет нас очень аккуратно соблюдать порядок передачи параметром, это несколько затрудняет процесс программирования, но зато Dual интерфейс работает значительно быстрее чем передача именованных параметров при позднем связывании сервера и контроллера автоматизации.
== Пример использования Dispatch интерфейса ==
Постановка задачи: получить информацию о документе, который открывается MS Word, при этом не должны использоваться никакие компоненты для доступа к серверам автоматизации.
Создадим новое приложение и выложим на форму кнопку. Пропишем событие onDoubleClick для кнопки следующим образом:
procedure TForm1.Button1Click(Sender: TObject);
var
 Shablon: OleVariant;
 word: _ApplicationDisp;
begin
 Shablon := ExtractFilePath(Application.EXEName) + 'shablon.DOC';
 word := CoWordapplicaTion.Create as _ApplicationDisp;
 (Word.Documents as DocumentsDisp).Open(Shablon, EmptyParam,
 EmptyParam, EmptyParam, EmptyParam, EmptyParam,
 EmptyParam, EmptyParam, EmptyParam, EmptyParam);
 ShowMessage((Word.Application as _Application).Get_Name + #13
 + ((Word.Application as _Application).ActiveDocument as _documentDisp).Path );
 word.Quit(EmptyParam, EmptyParam, EmptyParam);
end;
Переменная word имеет тип ApplicatiwonDisp = dispinterface ['{00020970-0000-0000-C000-000000000046}'], который описан в библиотеки типов Word97_TLB.Pas. Строка word := CoWordapplicaTion.Create as _ApplicationDisp; создает экземпляр сервера и возвращает DispInterface. Благодаря наличию библиотеки и жесткому приведению типов разработчик имеет возможность делать меньше ошибок, так как компилятор имеет определенную информацию о типах данных. В сравнении с использованием метода позднего связывания через функцию CreateOleObject этот метод более прогрессивный. По крайней мере, хотя бы часть информации компилятору да известна.
Подводя итог выше сказанному, можно отметить, что действительно использование компонент для доступа к COM серверам через VTable интерфейс значительно ускоряет работу приложения и ограждает разработчика от ошибок.
== Другие компоненты для доступа к COM серверам ==
Теперь поговорим о сервере Excel. После импортирования библиотеки типов, на закладке палитры компонент появились шесть иконок, основная из них ExcelApplication - компонента, обеспечивающая доступ к объекту Application сервера Excel. Следующий пример демонстрирует работу с объектом Application и WorkBook сервера Excel. Параметры серверу передаются по значению, кроме того, библиотека типов предоставляет все основные константы для работы с сервером:
const
 xlDays = $00000000;
 xlMonths = $00000001;
 xlYears = $00000002;
procedure TForm1.FormCreate(Sender: TObject);
begin
 ExcelApplication1.Workbooks.Add(ExtractFilePath(Application.EXEName) + 'customs', 0);
 ExcelWorkbook1.ConnectTo(ExcelApplication1.ActiveWorkbook);
 ExcelApplication1.Visible[0] := True;
end;
 
Таким образом, получив доступ к серверу, мы можем легко формировать всевозможные отчеты, строить графики. Другими словами из Delphi пользоваться ресурсами MS Excel.
== Работа с Outlook из Delphi ==
Так же как и в других серверах, в Outlook имеется класс Application, в который встраивается класс NameSpace. Последний предоставляет доступ к данным через объект класса MAPIFolders, представляющий собой коллекцию папок пользователя. Получив доступ к Outlook через компоненту OutLookApplication, извлекается объект доступа к MAPI папкам.
 mapi := OutlookApplication1.GetNamespace('MAPI');
 for i:=1 to mapi.Folders.Count do
 ListBox1.Items.Add(mapi.Folders.Item(i).Name);
Добраться до соответствующей папки теперь, нет проблем, так как мы имеем дело с вложенными объектами. В приложении к статье имеется пример извлечения содержимого всех папок сервера Microsoft OutLook.
Следующий пример производит рассылку писем, извлекая адреса из таблицы базы данных. Я вам предлагаю его самостоятельно доработать и привести к "товарному виду".
procedure TForm1.Button1Click(Sender: TObject);
var
 mapi: NameSpace;
begin
 Table1.Active := False;
 Table1.Active := True;
 // Получаем доступ к папке
 mapi := OutlookApplication1.GetNamespace('MAPI');
 while not Table1.Eof do
 begin
 // Подключаем объект класса TMailItem к новому элементу исходящих писем, для работы через VTable интерфейс
 MailItem1.ConnectTo(_DMailItem(mapi.Folders.Item(olPersonal).Folders.Item(olFolderOutbox).Items.Add(olPostItem) as IDispatch));
 // Наполняем новое письмо информацией
 MailItem1.Subject := 'test for ' + Table1Common_Name.Value;
 MailItem1.to_ := Table1Category.Value + '@elsite.ru';
 Table1Graphic.SaveToFile(Table1SpeciesNo.AsString + '.BMP');
 MailItem1.Attachments.Add(ExtractFilePath(Application.EXEName) + Table1SpeciesNo.AsString + '.BMP', 1, 1, Table1SpeciesName.Value);
 MailItem1.Body := #13#13 + 'Письмо с вложенным документом ' + #13 + MailItem1.SenderName;
 MailItem1.Sensitivity := olConfidential;
 MailItem1.FlagStatus := olFlagMarked;
 // Сохраняем письмо. OutLook самостоятельно его отошлет по почте
 MailItem1.Save;
 Table1.next;
 end;
end;
В заключении хочется еще раз подчеркнуть, что использование Vtable интерфейса значительно упростило процесс разработки контроллеров автоматизации COM серверов.
Используя мощный язык Delphi 5 и открытый интерфейс серверов MS Office можно строить очень большие и серьезные приложения, работающие совместно.
Author: И. Мирончик
ID: 04468
Category:COM
Category:MS Office
Category:Статьи