Попросили меня доработать старый модуль, который генерирует клиентам компании письма в формате MS Word. Пользователи создают шаблоны, в которых расставляют названия полей, заключенные в служебные символы (например, #CONTRACT_NUM#, #FIO#, #ADDRESS#...). Программа по заданным критериям выбирает информацию о клиентах из базы и генерирует письма, находя в тексте шаблонов названия полей и заменяя их фактическими значениями.
Если опустить все детали и различную логику, то упрощенно это выглядит так:
Var
MSWord: OleVariant;
i: Integer;
q: TSDQuery;
...
begin
...
MSWord := CreateOleObject('Word.Application');
MSWord.Documents.Add('какой то шаблон.dot');
For i := 0 to q.FieldCount-1 do
MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#', ReplaceWith := q.Fields[i].AsString);
...
MSWord.ActiveDocument.SaveAs('письмо любимому клиенту.doc');
...
Первое, с чем я столкнулся, это то, что длина значения параметра ReplaceWith не должна превышать 255 символов. Но это я обошел легко, заменив "поиск и замену текста" на "поиск и вывод текста":
If MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#') then
MSWord.Selection.TypeText(q.Fields[i].AsString);
Вторая задачка оказалась сложнее. Внизу листа в фиксированное место необходимо было вывести фамилию и адрес получателя. Сначала я думал поместить его в нижний колонтитул, но оказалось, что письмо может быть на двух листах. Тогда ничего не оставалось, как использовать объект "заметка". Вставил "заметку". Красота! Документ генерируется, текст сдвигается, заметка остается на месте... Но радость была недолгой, т.к. поля #FIO# и #ADDRESS#, помещенные в заметку, так и остались незамененными :(
Оказалось, что MSWord.Selection.Find.Execute ищет текст только в основной части документа, а в документе, состоящем из разных структурных элементов (заметок, колонтитулов, сносок и т.д.), поиск необходимо производить отдельно в каждом из этих элементов. Все эти структурные элементы документа являются элементами коллекции StoryRanges. Т.к. дело было к ночи, а модуль должен был быть готов к утру, я не стал разбираться, как работать со StoryRanges через OLE из Delphi, и просто добавил в тестовый шаблон письма макрос на VBA, в котором перебираются все структурные элементы активного документа, в которых ведется поиск:
Sub ReplaceText(sFindText As String, sReplaceText As String)
Dim rngStory As Range
For Each rngStory In ActiveDocument.StoryRanges
With rngStory.Find
.Text = sFindText
.Replacement.Text = sReplaceText
.Wrap = wdFindContinue
.Execute Replace:=wdReplaceAll
End With
Next rngStory
End Sub
А в программе я только написал вызов макроса:
MSWord.Application.Run('ReplaceText', '#FIO#', 'Иванов Иван Иванович');
Работает как часы :)
P.S. После окончания генерации документа, если MS Word вам больше не нужен, то не забывайте закрывать его и высвобождать память
MSWord.Quit;
MSWord := UnAssigned;
P.P.S. Для подобной задачи генерации писем в формат MS Word больше подходит не поиск и замена текста, использование полей с переменными (DocVariable) и закладок (Bookmark). А как это сделать, я расскажу в следующий раз.
Если опустить все детали и различную логику, то упрощенно это выглядит так:
Var
MSWord: OleVariant;
i: Integer;
q: TSDQuery;
...
begin
...
MSWord := CreateOleObject('Word.Application');
MSWord.Documents.Add('какой то шаблон.dot');
For i := 0 to q.FieldCount-1 do
MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#', ReplaceWith := q.Fields[i].AsString);
...
MSWord.ActiveDocument.SaveAs('письмо любимому клиенту.doc');
...
Первое, с чем я столкнулся, это то, что длина значения параметра ReplaceWith не должна превышать 255 символов. Но это я обошел легко, заменив "поиск и замену текста" на "поиск и вывод текста":
If MSWord.Selection.Find.Execute(FindText := '#' + q.Fields[i].FieldName + '#') then
MSWord.Selection.TypeText(q.Fields[i].AsString);
Вторая задачка оказалась сложнее. Внизу листа в фиксированное место необходимо было вывести фамилию и адрес получателя. Сначала я думал поместить его в нижний колонтитул, но оказалось, что письмо может быть на двух листах. Тогда ничего не оставалось, как использовать объект "заметка". Вставил "заметку". Красота! Документ генерируется, текст сдвигается, заметка остается на месте... Но радость была недолгой, т.к. поля #FIO# и #ADDRESS#, помещенные в заметку, так и остались незамененными :(
Оказалось, что MSWord.Selection.Find.Execute ищет текст только в основной части документа, а в документе, состоящем из разных структурных элементов (заметок, колонтитулов, сносок и т.д.), поиск необходимо производить отдельно в каждом из этих элементов. Все эти структурные элементы документа являются элементами коллекции StoryRanges. Т.к. дело было к ночи, а модуль должен был быть готов к утру, я не стал разбираться, как работать со StoryRanges через OLE из Delphi, и просто добавил в тестовый шаблон письма макрос на VBA, в котором перебираются все структурные элементы активного документа, в которых ведется поиск:
Sub ReplaceText(sFindText As String, sReplaceText As String)
Dim rngStory As Range
For Each rngStory In ActiveDocument.StoryRanges
With rngStory.Find
.Text = sFindText
.Replacement.Text = sReplaceText
.Wrap = wdFindContinue
.Execute Replace:=wdReplaceAll
End With
Next rngStory
End Sub
А в программе я только написал вызов макроса:
MSWord.Application.Run('ReplaceText', '#FIO#', 'Иванов Иван Иванович');
Работает как часы :)
P.S. После окончания генерации документа, если MS Word вам больше не нужен, то не забывайте закрывать его и высвобождать память
MSWord.Quit;
MSWord := UnAssigned;
P.P.S. Для подобной задачи генерации писем в формат MS Word больше подходит не поиск и замена текста, использование полей с переменными (DocVariable) и закладок (Bookmark). А как это сделать, я расскажу в следующий раз.
этот способ вообще не работает.
ОтветитьУдалитьвторой параметр, который здесь обозначен как ReplaceWith, может принимать только целочисленные значения.
Спасибо за комментарий. Зачем такие категоричные заявления "этот способ вообще не работает"? Пишите более точно "этот способ у меня не работает". А т.к. эти куски кода из моего проекта, то я могу с уверенностью в 100% сказать – "этот способ работает" :)
ОтветитьУдалитьда я сам дурак. заработало. мне IDE подчеркивало присвоения в параметрах типа FindText := ... вот я их и убрал. а без них никак. раньше никогда с таким синтаксисом не встречался.
ОтветитьУдалитьПишу проект на Lazarus, очень помогла Ваша статья, дала правильное направление. Спасибо!
ОтветитьУдалитьу меня ругается на TSDQuery, не подскажете какой модуль для этого нужно подключить?
ОтветитьУдалитьTSDQuery - это класс из SQLDirect и подключить надо SDEngine. TSDQuery - это аналог стандартного TQuery из DBTables. Вы можете использовать любого наследника DataSet.
ОтветитьУдалитьэто получается только чать кода?
ОтветитьУдалитьможно ли использовать эксель таблицу как базу данных?
1. Что значит часть кода? Это код, которого достаточно, что бы продемонстрировать, как сделать поиск и замену текста в документе Word из Delphi используя сам Word
ОтветитьУдалить2. Все, что угодно можно при желании рассматривать, как базу данных ;) Excel тем более. Можно прочитать страницу Excel в какую-нибудь MemTable и обрабатывать его в ней запросами, потом сохранить обратно (я так делал). А можно прочитать лист как таблицу. Возможно, это может сделать ODBC или еще какая библиотеки. Например, CData FireDAC Components for Excel https://www.cdata.com/kb/tech/excel-firedac-rad-vcl-app.rst
позволяет работать с листом Excel как с обычным DataSet
С exel оказалось все намного проще.
УдалитьВстроенная функция слияния нормально генерирует письма. Спасибо вам
Ясно, спасибо большое. Завтра попробую замутить. Из за многоточии думал, что код не целый для функционирования. Если завтра наткнулсь на проблемы можете проконсультировать по немножку, если да то оставьте телеграм пожалуйста:)
ОтветитьУдалить