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

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

96
Часто приходится заниматься созданием сложных документов 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/.

96

См. также

Комментарии
Сортировка: Древо
1. vladismi 160 12.12.17 11:27 Сейчас в теме
Запомним как упорядочение приемов.
Однако предложенное 1С использование копипаста втыкает в файл-приемник всю ту грязь, которую пользователь копирует у себя пока программа готовит вордовый документ...
"А мужики то не знают"...
:(
ger_kar; YPermitin; +2 Ответить
15. sulitckaja 22.02.18 06:30 Сейчас в теме
Может сможет подсказать кто, как в документе Word скопировать и вставить ниже уже существующую таблицу.
Делаю так:
Шаблон = Новый COMОбъект("Word.Application");
Шаблон.Documents.Open(ИмяФайлаПолное); 
Шаблон.Application.Documents(1).Content.Tables(1).Range.Copy();
Шаблон.Application.Documents(1).Content.InsertParagraphAfter();
Шаблон.Application.Documents(1).Content.Paste();


В результате таблица копируется но весь текст и предыдущая таблица исчезает.
16. wonderboy 142 22.02.18 07:41 Сейчас в теме
(15) Предполагаю что Content.Paste() делает вставку вместо всего контента. Вам наверное нужно вставить вместо последнего параграфа, который вы добавили.
Что-то вроде
КоличествоПараграфов = Шаблон.Application.Documents(1).Paragraphs.Count;
Шаблон.Application.Documents(1).Paragraphs(КоличествоПараграфов-1).Range.Paste()
sulitckaja; +1 Ответить
17. sulitckaja 22.02.18 08:01 Сейчас в теме
(16)Спасибо большое. Сейчас попробую.
18. sulitckaja 22.02.18 08:19 Сейчас в теме
(16)
КоличествоПараграфов = Шаблон.Application.Documents(1).Paragraphs.Count;
Шаблон.Application.Documents(1).Paragraphs(КоличествоПараграфов-1).Range.Paste()

(16)
Все получилось! А можно еще один вопрос? Как сделать так, чтобы таблица копировалась с нового листа?
19. wonderboy 142 22.02.18 09:06 Сейчас в теме
(18) Я вам с ходу не подскажу. Наверное что-то вроде

Шаблон.Application.Documents(1).Selection.EndKey(6);
Шаблон.Application.Documents(1).Selection.InsertBreak();

перед добавление параграфа.
sulitckaja; +1 Ответить
20. sulitckaja 22.02.18 09:24 Сейчас в теме
2. A1ice1990 110 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. A1ice1990 110 13.12.17 09:30 Сейчас в теме
(4) Здорово, но все еще медленнее чем чтение из потока двоичных данных)
3. badboychik 60 12.12.17 17:37 Сейчас в теме
Я сделал с полпинка на node.js - передаешь по GET или POST набор параметров "Ключ:Значение" и имя шаблона, в 1С приходит готовый docx, удобно когда на сервере нет офиса или COMОбъект не работал как у нас (наверно 64битность имеет значение)
6. Rustig 990 13.12.17 22:37 Сейчас в теме
автор "собаку съел" на шаблонах Word и автоматизации взаимодействия 1с и Word
респект
7. wonderboy 142 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. 132 19.01.18 11:49 Сейчас в теме
Хорошая статья и бесплатная !
9. rpgshnik 834 14.02.18 10:54 Сейчас в теме
Добрый день.
Смотрю вы работали плотно с WORD, может знаете как программно через 1С задать стиль всему документу по умолчанию 2003, вместо 2010?
10. milkers 2009 14.02.18 11:03 Сейчас в теме
(9) Нет, не приходилось сталкиваться. Может кто-нибудь из коллег подскажет.
11. wonderboy 142 15.02.18 09:01 Сейчас в теме
(9) Дмитрий, а как бы вы это вручную сделали?
12. rpgshnik 834 15.02.18 09:32 Сейчас в теме
(11) в ручную просто. Скриншот прилагаю.
Прикрепленные файлы:
13. wonderboy 142 15.02.18 10:49 Сейчас в теме
(12) Если знаете как сделать вручную - тогда включаете запись макроса, выполняете действие, останавливаете запись. Потом этот макрос открываете, смотрите как на VB эти действия делаются. Обычно этот код можно из 1С выполнить (с небольшими адаптациями).

Вот для вашего случая что получилось:
ActiveDocument.ApplyQuickStyleSet ("Word 2003")
14. rpgshnik 834 15.02.18 11:53 Сейчас в теме
(13) спасибо за идею!
Но решил отказаться от типового функционала по разбору ворда. Написать свой.
Я как понял баг заключался в том, что типовой функционал копирует участок текста и вставляет в свежий открытый документ. И естественно слитают стили. Но я ведь не могу всегда быть увереным что будет точно 2003... по этому буду как автор публикации ваят свой парсер ворда.
Хотя почти во всех конфигах даже старых типа КА1.1 есть модули по работе с шаблонами ворда. Эх.
Оставьте свое сообщение