Этюды по программированию. Взаимодействие с Microsoft Word

Программирование - Практика программирования

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

Этюды по программированию. Взаимодействие с Microsoft Word.

 

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

 

Получение шаблона.

В конфигурациях на основе БСП удобно хранить шаблон в справочнике ”Файлы”. Подсистема работы с присоединенными файлами позволяет назначить различные права на папки из этого справочника.

Файл можно найти по имени, или прикрепить  как дополнительный реквизит к справочникам и документам. (При создании дополнительного реквизита указать тип “Файл”).

Запрос = Новый Запрос;
	Запрос.Текст = 
	"ВЫБРАТЬ
	|	ДополнительныеСведения.Значение КАК Значение
	|ИЗ
	|	РегистрСведений.ДополнительныеСведения КАК ДополнительныеСведения
	|ГДЕ
	|	ДополнительныеСведения.Свойство.Наименование = &Наименование
	|	И ДополнительныеСведения.Объект = &Ссылка";
	
	Запрос.УстановитьПараметр("Ссылка", ДанныеПечати.Партнер);// Укажите ваш объект с прикрепленным дополнительным реквизитом шаблона.

	Запрос.УстановитьПараметр("Наименование", "Шаблон спецификации (Сделки с клиентами)"); //Укажите ваше название реквизита
	
	РезультатЗапроса = Запрос.Выполнить();
	
	ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
	
	Если  ВыборкаДетальныеЗаписи.Следующий() Тогда
		Файл= ВыборкаДетальныеЗаписи.Значение;
		ДанныеФайлаИДвоичныеДанные = РаботаСФайламиСлужебныйВызовСервера.ДанныеФайлаИДвоичныеДанные(Файл);
		
		ДанныеФайла = ДанныеФайлаИДвоичныеДанные.ДанныеФайла;
		ДвоичныеДанные = ДанныеФайлаИДвоичныеДанные.ДвоичныеДанные;
		ИмяВременногоФайла = КаталогВременныхФайлов()+"макет.mxl";
		ДвоичныеДанные.Записать(ИмяВременногоФайла);
		Макет =Новый ТабличныйДокумент;
		Макет.Прочитать(ИмяВременногоФайла); 
		КонецЕсли;
	КонецЕсли;


Открытие объекта  документа из временного файла.

       Word = Новый COMОбъект("Word.Application");
                Попытка
                               док = Word.Documents.Open(ИмяВременногоФайла);
                               Док.SaveAs(ИмяВременногоФайла);
                Исключение
                               СообщениеОбОшибке = НСтр("Файл шаблона, указанный в константе, не найден: "+ИмяВременногоФайла+"
                                                              |Подробности:'")
                                                   + КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
                               Word.Quit();
                               ВызватьИсключение СообщениеОбОшибке;
                КонецПопытки;
                РабочийКаталогПользователя = РаботаСФайламиСлужебныйКлиент.РабочийКаталогПользователя();
               
               
                Word.Visible = 1;
                Word.Options.CheckSpellingAsYouType = 0;
                Word.Options.CheckGrammarAsYouType =  0;
                Word.Options.CheckGrammarWithSpelling =  0;
                Selection = Word.Selection;

         Работа с таблицами:

          1. Макет таблицы. Создаем файл с шаблоном WORD. В файле должна быть таблица с шапкой, пустой строкой и (опционально) подвалом, например следующего вида:

    1. Поиск нужной таблицы в документе. Так как в документе обычно много таблиц, нужную таблицу нужно найти и заполнить
    Для Каждого Таб из док.Tables цикл
    Если СтрНайти(Таб.Cell(1, 1).Range.Text,"№")>0 И СтрНайти(Таб.Cell(1, 2).Range.Text,"Наименование")>0 Тогда
    //… Заполнение таблицы
           ТЗ= ПолучитьТЧТовары(Объект.СсылкаНаОбъект);
                           Таб.Rows(Таб.Rows.Count).Select();  //Выделям строку, на место которой будем вставлять новые строки
                           //Таб.Rows(1).Select(); 
                           Строка=Таб.Rows.Count;
                           Итерация=1;
                           Для Каждого стр Из ТЗ Цикл
                                           Если Итерация>1 Тогда
                                                          Selection.InsertRowsBelow( 1); //
                                           КонецЕсли;
                                           Таб.Cell(Строка, 1).Range.Text = стр.НомерСтроки;
                                           Таб.Cell(Строка, 2).Range.Text = стр.НоменклатураНаименование+" "+ТекстовоеОписаниеБезКомплектации(стр.ТекстовоеОписание);
                                           Таб.Cell(Строка, 2).Range.Paragraphs.Alignment = 0;
                                           текКол = ?(стр.Количество = 0,1,стр.Количество );
                                           Таб.Cell(Строка, 3).Range.Text =?((стр.ЦенаВключаетНДС), Формат((стр.Сумма - стр.СуммаНДС), "ЧДЦ=2"),Формат(стр.Сумма, "ЧДЦ=2"));
                                           Таб.Cell(Строка, 3).Range.Paragraphs.Alignment = 1;   
                                           Таб.Cell(Строка, 4).Range.Text =Формат(стр.СуммаНДС, "ЧДЦ=2");
                                           Таб.Cell(Строка, 4).Range.Paragraphs.Alignment = 1;   
                                           Таб.Cell(Строка, 5).Range.Text = ?((стр.ЦенаВключаетНДС), Формат((стр.Сумма ), "ЧДЦ=2"),Формат((стр.Сумма+ стр.СуммаНДС), "ЧДЦ=2"));
                                           Таб.Cell(Строка, 5).Range.Paragraphs.Alignment = 1;   
                                           Строка = Строка + 1;
                                          Итерация=Итерация+1;
                           КонецЦикла;
           КонецЦикла;
    Вставляем фрагмент из другого документа WORD:
    Обеспечиваем в нужном месте шаблона закладку.
    Процедура для вставки фрагмента:
    1. Процедура ВставитьПодшаблонИзСправочникаФайл(ИмяЗакладки,Word)
             Selection = Word.Selection;
             Для Каждого Закладка из Word.ActiveDocument.Bookmarks Цикл
                             Если Найти(Закладка.Name,ИмяЗакладки) Тогда
                                             Файл = НайтиФайлПодшаблонаНаСервере();
                                             Если ЗначениеЗаполнено(Файл) Тогда
                                                            ДанныеФайлаИДвоичныеДанные = РаботаСФайламиСлужебныйВызовСервера.ДанныеФайлаИДвоичныеДанные(Файл);
                                                            ДанныеФайла = ДанныеФайлаИДвоичныеДанные.ДанныеФайла;
                                                            ДвоичныеДанные = ДанныеФайлаИДвоичныеДанные.ДвоичныеДанные;
                                                            Попытка
                                                                            ИмяВременногоФайлаШаблона = КаталогВременныхФайлов()+"Шкафы КРУ сборка1.doc";
                                                                            ДвоичныеДанные.Записать(ИмяВременногоФайлаШаблона);
                                                            Исключение
                                                                            Попытка
                                                                                            ИмяВременногоФайлаШаблона = КаталогВременныхФайлов()+"Шкафы КРУ сборка2.doc";
                                                                                            ДвоичныеДанные.Записать(ИмяВременногоФайлаШаблона);
                                                                            Исключение
                                                                            КонецПопытки;
                                                            КонецПопытки;
                                                           
                                                           
                                                            Ворд2 = Новый COMОбъект("Word.Application");
                                                            Попытка
                                                                            док2 = Ворд2.Documents.Open(ИмяВременногоФайлаШаблона);
                                                                            НомерСтрокиТаб=3;
                                                                            Для Каждого Таб из док2.Tables цикл
                                                                            //заполняем таблицу        
                                                                            КонецЦикла;
                                                                           
                                                                            Ворд2.ActiveDocument.Select();
                                                                            Ворд2.Selection.Copy();
                                                                            Закладка.Range.Select();
                                                                            Selection.paste();
                                                                            Ворд2.ActiveDocument.Close(0);
                                                                            Ворд2.Quit();
                                                            Исключение
                                                                            ПредупреждениеСерв("Файл  не найден: "+ИмяВременногоФайлаШаблона);
                                                                            Ворд2.Quit();
                                                            КонецПопытки;
                                                            Попытка
                                                                            УдалитьФайлы(ИмяВременногоФайлаШаблона); 
                                                            Исключение
                                                                            ПредупреждениеСерв(ОписаниеОшибки());
                                                            КонецПопытки;
                                             КонецЕсли;
                             КонецЕсли;
             КонецЦикла;
      КонецПроцедуры
      Замена текста шаблона на нужный нам текст WORD:
      1. Предназначенный для замены текст шаблона ограничиваем квадратными скобками. Вместо текста пишем название маркера, по которому мы будем находить этот фрагмент.
      2.  В 1С создаем таблицу маркеров, которая будет содержать два обязательных поля “ Маркер” и “Значение”

Вызываем следующий код:

         Для Каждого стр Из Маркеры Цикл
                              Если ТипЗнч(стр.Значение) = Тип("Дата") Тогда
                                              Значение = СокрЛП(Формат(стр.Значение, "ДФ=""dd.MM.yyyy 'г.'"""));
                              Иначе
                                              Значение = СокрЛП(стр.Значение);
                              КонецЕсли;
                               док.content.find.execute("["+стр.Маркер+"]",,,,,,,,, Строка(Значение), 2);
                               док.Sections(1).Headers(1).Range.Find.Execute("["+стр.Маркер+"]",,,,,,,,, Строка(Значение), 2);
         КонецЦикла;
Вставка рисунка из макета:
  1. Создаем в конфигураторе макет-двоичные данные, в него загружаем нужный рисунок.
  2. Вызываем функцию, текст которой приведен ниже. В качестве аргумента selection удобно передавать выделение ячейки таблицы, это позволяет красиво расположить рисунок в тексте. Сама таблица может быть и невидимой. Например, следующий вызов:
 

///...

ВставитьМакет( Док,Таб.Cell(Строка, 3).Range, ИмяМакета);  

///...

&НаКлиенте

Функция ВставитьМакет( Знач Док,Знач Selection,Знач ИмяМакета)

               Перем Picture, Shape;

               Попытка

                               ИмяВременногоФайла= СохранитьМакетВоВременныйФайл(ИмяМакета);                            

                               Picture = Selection.InlineShapes.AddPicture(ИмяВременногоФайла,, Истина);

                               //Picture.LockAspectRatio = -1;      //сохрняем пропорции

                           Picture.Height= 150;

                               Picture.Width  = 150;              //устанавливаем ширину

                         // Чтобы установить обтекание текста, конвертируем рисунок в фигуру

                                  Shape = Picture.ConvertToShape();

                            Shape.WrapFormat.Type = 5;// перед текстом...

               Исключение

                               Сообщить("Не удалось подгрузить временный файл:"+ИмяВременногоФайла);

               КонецПопытки;

               Попытка

               УдалитьФайлы(ИмяВременногоФайла);

               Исключение

                               Сообщить("Не удалось удалить временный файл:"+ИмяВременногоФайла+"

                               |"+ОписаниеОшибки());

               КонецПопытки;

               // Зададим размер

               Возврат Selection;

КонецФункции

P.S.: Надеюсь, вам понравится эта и другие мои статьи и разработки на //infostart.ru/profile/48714/.

См. также

Комментарии
1. Владимир (vladismi) 158 12.12.17 11:27 Сейчас в теме
Запомним как упорядочение приемов.
Однако предложенное 1С использование копипаста втыкает в файл-приемник всю ту грязь, которую пользователь копирует у себя пока программа готовит вордовый документ...
"А мужики то не знают"...
:(
ger_kar; YPermitin; +2 Ответить
2. Plague Fox (A1ice1990) 59 12.12.17 16:23 Сейчас в теме
Вся эта объектная модель взаимодействия требует установленного ворда.
Была у меня идейка извратиться и написать взаимодействие с Word'овскими макетами через ЧтениеДанных для простых задач.
Работать должно в разы быстрее и не требовать офиса.

Раньше также делал дикую вложенность как у вас:
Для Каждого Закладка из Word.ActiveDocument.Bookmarks Цикл
	Если Найти(Закладка.Name,ИмяЗакладки) Тогда
		Если ЗначениеЗаполнено(Файл) Тогда
			...
		КонецЕсли; // Если ЗначениеЗаполнено(Файл)
	КонецЕсли; // Если Найти(Закладка.Name,ИмяЗакладки)
КонецЦикла; // Для Каждого Закладка из Word.ActiveDocument.Bookmarks
Показать


Попробуйте такую запись, она легче читается:
Для Каждого Закладка из Word.ActiveDocument.Bookmarks Цикл
	Если Не Найти() Тогда Продолжить КонецЕсли;
	Если Не ЗначениеЗаполнено(Файл) Тогда Продолжить КонецЕсли;
	...
КонецЦикла; // Для Каждого Закладка из Word.ActiveDocument.Bookmarks


Можно и от цикла избавиться с помощью рекурсивных процедур или goto, но это уже извращение)))
ger_kar; vz1987; sansys; biformatus; +4 Ответить
4. Ийон Тихий (cool.vlad4) 43 13.12.17 01:21 Сейчас в теме
(2) (3) я писал COM сервер на C# по прикручиванию https://habrahabr.ru/post/269307/ , работает быстрее некуда, работает на сервере, из минусов только, что нужно особым образом делать шаблоны и нет универсальности, поэтому и не выкладываю
5. Plague Fox (A1ice1990) 59 13.12.17 09:30 Сейчас в теме
(4) Здорово, но все еще медленнее чем чтение из потока двоичных данных)
3. Сергей Видякин (badboychik) 54 12.12.17 17:37 Сейчас в теме
Я сделал с полпинка на node.js - передаешь по GET или POST набор параметров "Ключ:Значение" и имя шаблона, в 1С приходит готовый docx, удобно когда на сервере нет офиса или COMОбъект не работал как у нас (наверно 64битность имеет значение)
6. г. Казань Рустем Гумеров (Rustig) 838 13.12.17 22:37 Сейчас в теме
автор "собаку съел" на шаблонах Word и автоматизации взаимодействия 1с и Word
респект
7. Владимир Харин (wonderboy) 29 18.01.18 10:04 Сейчас в теме
Если разрешите - несколько замечаний / дополнений:

1. Вы закладку ищете в цикле. Это лучше делать так:

Если WordFile.Bookmarks.exists(ИмяЗакладки) Тогда
	ТекЗакладка = WordFile.Bookmarks.Item(ИмяЗакладки);
...


2. По замене маркеров на нужный текст
У Вас предлагается поиск и замена, но не учитывается, что таким образом можно вставить текст только до 250 символов
Если текст больше - будет исключение. Приходится его разбивать на части и вставлять кусками - так было реализовано в 1С: Документообороте раньше.
Сейчас в БСП сделано концептуально правильно. См. общий модуль УправлениеПечатьюMSWordКлиент, процедура Заменить. Через Selection.TypeText(...).

И я бы рекомендовал использовать все же закладки. В Word-документе кроме основного контента и колонтитулов есть еще и другие Story (например, текст в надписях графических объектов). В каждой Story нужно отдельно поиск выполнять.

Есть универсальное решение
https://infostart.ru/public/662990/

Отлаженное, на его внедрениях тоже много Word'овских собак съедено :)
Serg O.; milkers; +2 Ответить
8. Сергей Огородников (Serg O.) 133 19.01.18 11:49 Сейчас в теме
Хорошая статья и бесплатная !
Оставьте свое сообщение