Шаг 396 - Автоматизация приложений MS Office. Часть 3.

Шаг прислал Yegor A. Blackheel (blackheel@rlt.ru)

Чтение документа

Следующим шагом, который мы рассмотрим, будет загрузка сохраненного документа. Для загрузки используется метод Open коллекции Documents. Вот его синтаксис:

LPDISPATCH Open(VARIANT* FileName, VARIANT* ConfirmConversions, VARIANT* ReadOnly, 
VARIANT* AddToRecentFiles, VARIANT* PasswordDocument, VARIANT* PasswordTemplate, 
VARIANT* Revert, VARIANT* WritePasswordDocument, VARIANT* WritePasswordTemplate, 
VARIANT* Format, VARIANT* Encoding, VARIANT* Visible);

Как мы видим, параметров у этого метода весьма много. К счастью, большинство названий говорит само за себя и очевидно, часть из них можно оставить пустыми, то есть не использовать. Для самого простого примера нам понадобятся: имя файла (VARIANT* FileName), флаг автоматического или с подтверждением преобразования файла из "неродного" формата (VARIANT* ConfirmConversions), флаг открытия документа в режиме "только для чтения" (VARIANT* ReadOnly) и формат открываемого файла (VARIANT* Format). Назначение остальных параметров для простоты можно проигнорировать. Следует отметить, однако, что из всех приведенных параметров Format может принимать следующие значения:

typedef enum {
    wdOpenFormatAuto = 0,
    wdOpenFormatDocument = 1,
    wdOpenFormatTemplate = 2,
    wdOpenFormatRTF = 3,
    wdOpenFormatText = 4,
    wdOpenFormatUnicodeText = 5,
    wdOpenFormatEncodedText = 5,
    wdOpenFormatAllWord = 6,
    wdOpenFormatWebPages = 7
} WdOpenFormat;

Для простоты в примере используем wdOpenFormatAuto, давая тем самым Word-у возможность самому определить формат. Установим ConfirmConversions в TRUE, на тот случай, если у него возникнут сомнения :) :

Пример:

//...................................
//Open existing document
Documents oDocs;
_Document oDoc;
oDocs = oWordApp.GetDocuments();
oDocs.Open(COleVariant("c:\\doc1.doc"),vTrue,vFalse,vFalse,COleVariant(""),COleVariant(""),
vFalse,COleVariant(""),COleVariant(""),COleVariant(short(wdOpenFormatAuto)),COleVariant(""),vTrue);
//...................................

Таблицы...

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

Для этого нам понадобятся следующие коллекции и объекты: Tables - коллекция таблиц в документе, Table - объект "таблица", Range - диапазон, Cell - ячейка. Сценарий работы с таблицами идентичен описанному в предыдущих частях: получить коллекцию, добавить в нее объект, работать с этим объектом.

Tables oTables;
Table oTable;
Range oRan;
// получить диапазон, на базе которого будет создана таблица
oRan = oPar.GetRange();
oTables = oDoc.GetTables();
//добавить таблицу в коллекцию
oTable = oTables.Add(oRan,20,3,COleVariant((short)wdWord8TableBehavior), 
COleVariant((short)wdAutoFitFixed));

Cell oCell;
// получить ячейку: пятая строка, второй столбец
oCell = oTable.Cell(5,2);
// теперь ячейка - диапазон для ввода текста
oRan = oCell.GetRange();
oRan.SetText("Ячейка 5,2");

При создании таблицы используются параметры "поведения" таблицы (стиль 97 или 2000 Word-а, а также вариант подгонки по ширине). Ниже приведен их полный список:

typedef enum {
	wdWord8TableBehavior = 0,
	wdWord9TableBehavior = 1
} WdDefaultTableBehavior;

typedef enum {
	wdAutoFitFixed = 0,
	wdAutoFitContent = 1,
	wdAutoFitWindow = 2
} WdAutoFitBehavior;

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

...и границы

Для работы с границами нам понадобятся коллекция Borders и объект Border, представляющий собой одну границу. Кроме того, нам понадобятся стили линий:

typedef enum {
	wdLineStyleNone = 0,
	wdLineStyleSingle = 1,
	wdLineStyleDot = 2,
	wdLineStyleDashSmallGap = 3,
	wdLineStyleDashLargeGap = 4,
	wdLineStyleDashDot = 5,
	wdLineStyleDashDotDot = 6,
	wdLineStyleDouble = 7,
	wdLineStyleTriple = 8,
	wdLineStyleThinThickSmallGap = 9,
	wdLineStyleThickThinSmallGap = 10,
	wdLineStyleThinThickThinSmallGap = 11,
	wdLineStyleThinThickMedGap = 12,
	wdLineStyleThickThinMedGap = 13,
	wdLineStyleThinThickThinMedGap = 14,
	wdLineStyleThinThickLargeGap = 15,
	wdLineStyleThickThinLargeGap = 16,
	wdLineStyleThinThickThinLargeGap = 17,
	wdLineStyleSingleWavy = 18,
	wdLineStyleDoubleWavy = 19,
	wdLineStyleDashDotStroked = 20,
	wdLineStyleEmboss3D = 21,
	wdLineStyleEngrave3D = 22,
	wdLineStyleOutset = 23,
	wdLineStyleInset = 24
} WdLineStyle;

Работать с границами можно в рамках как одной отдельной ячейки, так и диапазона ячеек. Рассмотрим первый вариант. Пусть нам необходимо отключить нижнюю и правую границы у ячейки (5,2), а верхнюю сделать тройной.

// получить ячейку
oCell = oTable.Cell(5,2);
Borders oBorders;
Border oBorder;
// получить границы ячейки
oBorders = oCell.GetBorders();
// получить нижнюю границу
oBorder = oBorders.Item(wdBorderBottom);
// сделать ее невидимой
oBorder.SetVisible(FALSE); 
// получить нижнюю границу
oBorder = oBorders.Item(wdBorderRight);
// сделать ее невидимой
oBorder.SetVisible(FALSE); 
// получить верхнюю границу
oBorder = oBorders.Item(wdBorderTop);
// поменять стиль её линии
oBorder.SetLineStyle(wdLineStyleTriple); 

Если заведомо известно, что надо менять все границы разом у одной ячейки, то проще воспользоваться методом SetEnable(long nNewValue) коллекции oBorders. Например, сделать все границы ячейки (1,1) двойной волнистой линией.

oCell = oTable.Cell(1,1);
oBorders = oCell.GetBorders();
oBorders.SetEnable(wdLineStyleDoubleWavy); 

Слияние ячеек осуществляется методом Merge(LPDISPATCH MergeTo). В качестве параметра указывается ячейка, с которой надо слить данную.

Пример: пусть надо слить ячейки (1,2) и (1,3).

oCell = oTable.Cell(1,2);
Cell oSecondCell = oTable.Cell(1,3);
oCell.Merge(oSecondCell);

Для разбиения ячеек служит метод Split(VARIANT* NumRows, VARIANT* NumColumns) объекта Cell. В качестве параметров задаётся, на сколько строк и столбцов разбивается ячейка. Разобьём ячейку (3,1) на 4 столбца и 2 строки:

oCell = oTable.Cell(3,1);
oCell.Split(COleVariant(short(2)),COleVariant(short(4)));

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

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

Рассмотрим пример создания таблицы 10х3 с помощью операции "Преобразовать в таблицу". Вот синтаксис соответствующего метода объекта Selection: ConvertToTable(VARIANT* Separator, VARIANT* NumRows, VARIANT* NumColumns, VARIANT* InitialColumnWidth, VARIANT* Format, VARIANT* ApplyBorders, VARIANT* ApplyShading, VARIANT* ApplyFont, VARIANT* ApplyColor, VARIANT* ApplyHeadingRows, VARIANT* ApplyLastRow, VARIANT* ApplyFirstColumn, VARIANT* ApplyLastColumn, VARIANT* AutoFit, VARIANT* AutoFitBehavior, VARIANT* DefaultTableBehavior).

//................................
oSel = app.GetSelection();
// переместиться вниз до следующей секции (без выделения)
oSel.MoveDown(COleVariant(short(wdParagraph)),COleVariant(long(wdForward)),COleVariant(short(wdMove)));
// добавить 3 параграфа
oSel.TypeText("\n\n\n");
// сформировать текст, разделенный знаками табуляции (на столбцы)
// и символами перевода строки (на строки)
CString sText;	
for (int i=1; i<=10; i++)
{
	sText.Format("Ячейка %d,1\tЯчейка %d,2\tЯчейка %d,3\n",i,i,i);
	oSel.TypeText(sText);
}
// подняться вверх к началу текста, при этом выделиь его
oSel.MoveUp(COleVariant(short(wdParagraph)),COleVariant(long(10)),COleVariant(short(wdExtend)));
// преобразовать выделенyый текст в таблицу
oSel.ConvertToTable(
	COleVariant("\t"),				// знак разделителя стольбцов
	COleVariant((long)10),			// 10 строк
	COleVariant((long)3),			// 3 столбца
	covOptional,
	COleVariant((long)wdTableFormatNone), 	// без применения предопределенного стиля таблицы
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	covTrue,
	COleVariant((short)wdAutoFitContent), 
	COleVariant((short)wdWord9TableBehavior));
// переместиться за таблицу вниз, сняв выделение
oSel.MoveDown(COleVariant(short(wdParagraph)),COleVariant(long(wdForward)),
		COleVariant(short(wdMove)));

Конечно, мы рассмотрели далеко не все возможности таблиц. Но тем интереснее будет самостоятельная работа :)

Кто ищет, тот всегда найдет...

Часто у людей, автоматизирующих Word возникает необходимость осуществить поиск (а иногда и замену) информации в текстовых документах. Естественно, данная возможность предусмотрена, и сейчас мы рассмотрим пример поиска.

За поиск отвечает объект Find, который включает в себя как свойства поиска, так и методы. Порограммист может управлять всеми теми параметрами, которые мы видим в окошке "Найти" Word-а.

Получить объект Find можно, запросив его у объектов Selection или Range.

CFind oFind = oSel.GetFind();			

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

oFind.ClearFormatting();
oFind.ClearAllFuzzyOptions();

Теперь для простого поиска достотчно вызвать метод Execute. Вот его синтаксис:
BOOL Execute(VARIANT * FindText, VARIANT * MatchCase, VARIANT * MatchWholeWord, VARIANT * MatchWildcards, VARIANT * MatchSoundsLike, VARIANT * MatchAllWordForms, VARIANT * Forward, VARIANT * Wrap, VARIANT * Format, VARIANT * ReplaceWith, VARIANT * Replace, VARIANT * MatchKashida, VARIANT * MatchDiacritics, VARIANT * MatchAlefHamza, VARIANT * MatchControl).

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

// поскольку будем искать вперед по тексту, следует переместить текущую позцию курсора вверх 
// за таблицу, используя, например, такую конструкцию
oSel.MoveUp(COleVariant(short(wdParagraph)),COleVariant(long(wdForward)),COleVariant(short(wdMove)));  

if (oFind.Execute(COleVariant("Ячейка 10,3"),	// что ищем (ищем текст "Ячейка 10,3")
	covFalse,				// учитывать регистр (нет)
	covTrue,				// слово целиком (да)
	covFalse,				// использовать подстановочные знаки ? (нет)
	covFalse,				// произносится как ? (нет, надо точно)
	covFalse,				// все словоформы ? (нет)
	covTrue, 				// поиск вперед по тексту ? (да)
	COleVariant(short(wdFindContinue)),	// как искать	(найти и продолжить поиск)
	covFalse,				// поиск по формату? (нет)
	COleVariant(""),			// на что заменять (ни на что)
	covFalse,				// поиск и замена? (нет, только поиск)
	covFalse,				// прочие параметры. в принципе для русского языка несущественные
	covFalse,				// --"--
	covFalse,				// --"--
	covFalse				// --"--
	))
		AfxMessageBox("Найден текст \"Ячейка 10,3\"");
	else
		AfxMessageBox("Текст \"Ячейка 10,3\" не найден");

Общие замечания и рассуждения (вместо заключения)

Естественным вопросом является следующий: насколько эффективно использовать приложения MS Officе для подготовки документов с использованием средств автоматизации? Ответить на него непросто. Изложенный в статье подход обладает своими достоинствами и недостатками. К достоинствам можно причислить относительную простоту автоматизации, возможность использовать практически все функции MS Officе, включая процедуры проверки орфографии и сложных расчетных операций (например, в MS Excel), возможность выполнять все действия "невидимо" для пользователя, наличие MS Office почти (!) на каждом рабочем месте, автоматизация не требует покупки никаких сторонних компонентов и библиотек.

Однако, недостатки тоже существенны. Это малая документированность методов (немного компенсируется тем, что имена методов, объектов и коллекций являются self-explanatory, т.е. самоочевидными), тот или иной компонент MS Officе может быть и не установлен на какой-то машине, что делает невозможным использование процедур с использованием автоматизации, низкая скорость работы (особенно если все действия происходят в видимом окне) .

Существуют ли альтернативные возможности подготовки отчетов и документов, совместимых с MS Office? Конечно. Можно воспользоваться, например, платными компонентами для Delphi, которые обладают изрядной гибкостью при подготовке документов за счет использования полей (fiеlds). Можно использовать коммерческие продукты типа Crystal Reports. Или просто "вручную" экспортировать всю информацию в формате RTF.

Благодарности

Спасибо всем читателям, присылавшим вопросы после выхода первых частей статьи.
Спасибо сайту Первые шаги и лично Каеву Артёму за публикации.

Литература и инструменты

  1. Н. З. Елманова, С. В. Трепалин. DELPHI 4: технология COM., М.: Диалог-МИФИ, 1999, 320c. (ISBN 5-86404-127-0)
  2. OLEView.exe, утилита для просмотра информации (интерфейсы, ID, библиотеки типов, и т.д.) о СОМ-объектах, входит в поставку MS Visual C++. Должна также входить в комплект Delphi.

Исходники

Здесь вы можете загрузить иллюстрирующий проект.
Скачать исходники - архив ZIP,~70 Кб

Шаг прислал Yegor A. Blackheel (blackheel@rlt.ru)


Загрузить проект | Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Каев Артем - 08.04.2008