15 марта 2010

Использование полей и закладок при работе с MS Word из Delphi

   В предыдущей заметке "Поиск и замена текста в документе MS Word из Delphi" я рассказывал, как дорабатывал старый модуль, который генерирует клиентам компании письма в формате MS Word с помощью поиска и замены текста. Сдав модуль заказчикам, я в свободное от работы время, переделал его. Вместо поиска и замены использовал поля с переменными (DocVariable).
   В шаблон письма с помощью макроса добавил переменные

Sub AddFields()
  ThisDocument.Variables.Add "FIO", "FIO"
  ThisDocument.Variables.Add "ADDRESS", "ADDRESS"
  ...
End Sub

и расставил поля по тексту шаблона. В макросе у метода Add первый параметр – название переменной, а второй – ее значение. Я специально сделал их одинаковыми, чтобы пользователям было проще и нагляднее редактировать шаблоны.
   Затем внес изменения в методы модуля работающие с MS Word. Если опустить все детали и различную логику, то упрощенно работа с MS Word выглядит так:

Var
  wa: WordApplication;
  ovDotName, ovFileName: OleVariant;
  i: Integer;
  q: TSDQuery;
  ...
begin
  ...
  wa := CreateComObject(CLASS_WordApplication) as _Application;
  ovDotName := 'какой то шаблон.dot';
  wa.Documents.Add(ovDotName, EmptyParam, EmptyParam, EmptyParam);

  For i := 0 to q.FieldCount-1 do
    MSWordSetVariable(q.Fields[i].FieldName, q.Fields[i].AsString);
  ...
  ovFileName := 'письмо любимому клиенту.doc';
  wa.ActiveDocument.SaveAs(ovFileName, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam);
  ...

Где MSWordSetVariable – процедура, которая присваивает значение переменной в документе MS Word.

procedure MSWordSetVariable(ovVariable: OleVariant; const sValue: String);
begin
  If sValue = ''
    then wa.ActiveDocument.Variables.Item(ovVariable).Value := ' '
    else wa.ActiveDocument.Variables.Item(ovVariable).Value := sValue
end;

   Если переменной MS Word присвоить пустую строчку, то в поле выводится текст 'Ошибка! Переменная документа не указана.', поэтому вместо пустого значения я присваиваю ей пробел (так же делает и сам MS Word). Это связано со странной работой MS Word с коллекцией переменных. Если переменной присвоить пустую строку, то количество переменных в коллекции (Variables.Count) уменьшается на единицу, а попытка удалить переменную (Variables.Item(ovVariable).Delete) дает ошибку 'Объект был удален', т.е. присвоение пустой строки равносильно удалению. При этом после удаления переменной из коллекции переменных, присвоение ей непустого значения выполняется успешно и количество переменных в коллекции увеличивается на единицу, т.е. присвоение непустого значения равносильно вызову метода добавления переменной в коллекцию переменных.
   Продемонстрирую вышесказанное примером кода, по которому видно как изменяется количество переменных (iCount):

iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Value := '';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Value := 'не пусто';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Delete;
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Value := 'не пусто';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 3
wa.ActiveDocument.Variables.Item(ovVariable).Value := '';
iCount := wa.ActiveDocument.Variables.Count; // iCount = 2
wa.ActiveDocument.Variables.Item(ovVariable).Delete; // ошибка 'Объект был удален'

   Вот такая логика у индусов, писавших этот кусок MS Word.

   После присвоения значений всем переменным, осталось только дать команду полям обновиться новыми значениями переменных. Для этого в VBA у коллекции объектов полей (Fields) есть метод Update:

wa.ActiveDocument.Fields.Update

А для того, чтобы избежать дальнейшего обновления полей, избавимся от их связи с переменными:

wa.ActiveDocument.Fields.Unlink

   У каждого структурного элемента документа (заметки, колонтитула, сноски и т.д.) своя коллекция объектов полей, поэтому, если документ, как у меня, состоит из разных структурных элементов, то методы Update и Unlink необходимо вызвать для каждого из этих элементов. Для этого перебираем все элементы коллекции StoryRanges.

procedure MSWordUpdateStoryRanges;
  Var
    StoryRanges: Word2000.StoryRanges;
    StoryRange: Word2000.Range;
    iStoryIndex: integer;
Begin
  StoryRanges := wa.ActiveDocument.StoryRanges;
  For iStoryIndex := wdMainTextStory to wdFirstPageFooterStory do
    Try
      StoryRange := StoryRanges.Item(iStoryIndex);
      If StoryRange <> nil then
        begin
          StoryRange.Fields.Update;
          StoryRange.Fields.Unlink;

          While (StoryRange.NextStoryRange <> nil) do
            begin
              StoryRange := StoryRange.NextStoryRange;
              StoryRange.Fields.Update;
              StoryRange.Fields.Unlink;
            end;
          end;
    Except
      StoryRange := nil;
    End;
End;

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

procedure MSWordUpdateStoryRange(const iStoryIndex: integer);
  Var
    StoryRange: Word2000.Range;
begin
  Try
    StoryRange := wa.ActiveDocument.StoryRanges.Item(iStoryIndex);
    If StoryRange <> nil then
      begin
        StoryRange.Fields.Update;
        StoryRange.Fields.Unlink;
      end;
  Except
  End;
end;

И вызываю ее перед сохранением документа.

MSWordUpdateStoryRange(wdMainTextStory);
MSWordUpdateStoryRange(wdTextFrameStory);

   Все бы ничего, но часть текста передаваемого из программы в MS Word необходимо было выводить жирным шрифтом. А это через переменные не сделать :( В подобном случае "поиск и замену текста" можно заменить на "переход к закладке и вывод текста". Например, в шаблон вставляем закладку с именем 'писать текст сюда', а в программе пишем:

Var
  ovBookmarkName, ovWhat: OLEVariant;
begin
  ovWhat := wdGoToBookmark;
  ovBookmarkName := 'писать текст сюда';
  Try
    If wa.Selection.GoTo_(ovWhat, EmptyParam, EmptyParam, ovBookmarkName) <> nil then
      begin
        wa.Selection.TypeText('просто текст ');
        wa.Selection.Font.Bold := 1;
        wa.Selection.TypeText('жирный текст');
        wa.Selection.Font.Bold := 0;
        wa.Selection.TypeText(' просто текст');
      end;
  Except
  End;

   Вот и все :)
   Раннее связывание и использование полей/закладок дало существенный рост скорости генерации писем. Если на 500-х различных документах этого было почти не заметно, то при генерации 15 000 документов, прирост скорости составил 30% (специально проверил несколько раз на одних и тех же исходных данных).

P.S. При работе с ранним связыванием мне не нравится только одно – многие параметры в методах объявлены, как VAR (даже индекс элемента коллекции!), поэтому приходится заводить для них специальную переменную типа OleVariant.

3 комментария:

  1. спасибо тебе.
    помог в 2018 от одной баги с мерге полями. индусы они такие ))

    ОтветитьУдалить
  2. можете поделиться программой ? не могу разобратся с кодом

    ОтветитьУдалить
  3. Извините, но прошло уже больше 10 лет - проект давно закончен и исходные тексты остались в далеком прошлом.

    ОтветитьУдалить