Создание документов Word (в Excel VBA) из серии шаблонов документов
Привет всем. Я постараюсь сделать это кратко и просто. :)
Я
- 40 или около того шаблонных документов word с рядом полей (имя, адрес и т. д.), которые необходимо заполнить. Это исторически делается вручную, но это повторяется и громоздко.
- книги, когда пользователь заполнил огромный набор информации о человеке.
мне нужно
- способ программно (из Excel VBA) откройте эти шаблонные документы, отредактируйте значение полей из различных именованных диапазонов в книге и сохраните заполненные шаблоны в локальной папке.
Если бы я использовал VBA для программного редактирования определенных значений в наборе электронных таблиц, я бы отредактировал все эти электронные таблицы, чтобы содержать набор именованных диапазонов, которые могут использоваться во время процесса автозаполнения, но я не знаю о какой-либо функции "именованное поле" в Word документ.
Как я могу редактировать документы и создавать процедуру VBA, чтобы я мог открывать каждый документ, искать набор полей, которые могут потребоваться для заполнения, и заменять значение?
например, то, что работает как:
for each document in set_of_templates
if document.FieldExists("Name") then document.Field("Name").value = strName
if document.FieldExists("Address") then document.Field("Name").value = strAddress
...
document.saveAs( thisWorkbook.Path & "GeneratedDocs " & document.Name )
next document
вещи, которые я рассмотрел:
- слияние почты-но этого недостаточно, потому что для этого требуется открыть каждый документ вручную и структурировать книгу как источник данных, я хочу противоположный. Шаблоны являются источником данных, и книга повторяет их. Кроме того, mail merge предназначен для создания множества идентичных документов с использованием таблицы разных данных. У меня много документов, в которых используются одни и те же данные.
- использование текста-заполнителя, такого как "#NAME#", и открытие каждого документа для поиска и замены. Это решение, к которому я бы прибегнул, если ничего более элегантного не предлагается.
4 ответов
прошло много времени с тех пор, как я задавал этот вопрос, и мое решение подвергалось все большей и большей доработке. Мне приходилось иметь дело со всевозможными специальными случаями, такими как значения, которые поступают непосредственно из книги, разделы, которые должны быть специально созданы на основе списков, и необходимость делать замены в верхних и нижних колонтитулах.
как оказалось, недостаточно было использовать закладки, так как пользователи могли позже редактировать документы для изменения, добавления и удаления значения заполнителей из документов. Решение было на самом деле использовать ключевые слова например:
это просто страница из образца документа, который использует некоторые из возможных значений, которые могут автоматически вставляться в документ. Существует более 50 документов с совершенно разными структурами и макетами, использующими разные параметры. Единственный общему знанию документы Word и таблицы Excel-знание что эти значения-заполнители должны представлять. В excel это хранится в списке ключевых слов генерации документа, которые содержат ключевое слово, а затем ссылку на диапазон, который фактически содержит это значение:
это были ключевые два необходимых ингредиента. Теперь с помощью некоторого умного кода все, что мне нужно было сделать, это перебрать каждый генерируемый документ, а затем перебрать диапазон всех известных ключевых слов и выполнить поиск и замену для каждое ключевое слово в каждом документе.
во-первых, у меня есть метод оболочки, который заботится о поддержании экземпляра microsoft word, повторяющего все документы, выбранные для генерации, нумерации документов и выполнения пользовательского интерфейса (например, обработка ошибок, отображение папки пользователю и т. д.)
' Purpose: Iterates over and generates all documents in the list of forms to generate
' Improves speed by creating a persistant Word application used for all generated documents
Public Sub GeneratePolicy()
Dim oWrd As New Word.Application
Dim srcPath As String
Dim cel As Range
If ERROR_HANDLING Then On Error GoTo errmsg
If Forms.Cells(2, FormsToGenerateCol) = vbNullString Then _
Err.Raise 1, , "There are no forms selected for document generation."
'Get the path of the document repository where the forms will be found.
srcPath = FindConstant("Document Repository")
'Each form generated will be numbered sequentially by calling a static counter function. This resets it.
GetNextEndorsementNumber reset:=True
'Iterate over each form, calling a function to replace the keywords and save a copy to the output folder
For Each cel In Forms.Range(Forms.Cells(2, FormsToGenerateCol), Forms.Cells(1, FormsToGenerateCol).End(xlDown))
RunReplacements cel.value, CreateDocGenPath(cel.Offset(0, 1).value), oWrd
Next cel
oWrd.Quit
On Error Resume Next
'Display the folder containing the generated documents
Call Shell("explorer.exe " & CreateDocGenPath, vbNormalFocus)
oWrd.Quit False
Application.StatusBar = False
If MsgBox("Policy generation complete. The reserving information will now be recorded.", vbOKCancel, _
"Policy Generated. OK to store reserving info?") = vbOK Then Push_Reserving_Requirements
Exit Sub
errmsg:
MsgBox Err.Description, , "Error generating Policy Documents"
End Sub
это рутинные вызовы RunReplacements
который заботится об открытии документа, подготовке среды для быстрой замены, обновлении ссылок после этого, обработка ошибок и т. д.:
' Purpose: Opens up a document and replaces all instances of special keywords with their respective values.
' Creates an instance of Word if an existing one is not passed as a parameter.
' Saves a document to the target path once the template has been filled in.
'
' Replacements are done using two helper functions, one for doing simple keyword replacements,
' and one for the more complex replacements like conditional statements and schedules.
Private Sub RunReplacements(ByVal DocumentPath As String, ByVal SaveAsPath As String, _
Optional ByRef oWrd As Word.Application = Nothing)
Dim oDoc As Word.Document
Dim oWrdGiven As Boolean
If oWrd Is Nothing Then Set oWrd = New Word.Application Else oWrdGiven = True
If ERROR_HANDLING Then On Error GoTo docGenError
oWrd.Visible = False
oWrd.DisplayAlerts = wdAlertsNone
Application.StatusBar = "Opening " & Mid(DocumentPath, InStrRev(DocumentPath, "\") + 1)
Set oDoc = oWrd.Documents.Open(Filename:=DocumentPath, Visible:=False)
RunAdvancedReplacements oDoc
RunSimpleReplacements oDoc
UpdateLinks oDoc 'Routine which will update calculated statements in Word (like current date)
Application.StatusBar = "Saving " & Mid(DocumentPath, InStrRev(DocumentPath, "\") + 1)
oDoc.SaveAs SaveAsPath
GoTo Finally
docGenError:
MsgBox "Un unknown error occurred while generating document: " & DocumentPath & vbNewLine _
& vbNewLine & Err.Description, vbCritical, "Document Generation"
Finally:
If Not oDoc Is Nothing Then oDoc.Close False: Set oDoc = Nothing
If Not oWrdGiven Then oWrd.Quit False
End Sub
эта процедура затем вызывает RunSimpleReplacements
. и RunAdvancedReplacements
. В первом случае мы перебираем набор ключевых слов генерации документов и вызываем WordDocReplace
если документ содержит нашего сайта. Обратите внимание, что гораздо быстрее попробовать и Find
куча слов, чтобы выяснить, что они не существуют, а затем вызвать replace без разбора, поэтому мы всегда проверяем, существует ли ключевое слово, прежде чем пытаться его заменить.
' Purpose: While short, this short module does most of the work with the help of the generation keywords
' range on the lists sheet. It loops through every simple keyword that might appear in a document
' and calls a function to have it replaced with the corresponding data from pricing.
Private Sub RunSimpleReplacements(ByRef oDoc As Word.Document)
Dim DocGenKeys As Range, valueSrc As Range
Dim value As String
Dim i As Integer
Set DocGenKeys = Lists.Range("DocumentGenerationKeywords")
For i = 1 To DocGenKeys.Rows.Count
If WordDocContains(oDoc, "#" & DocGenKeys.Cells(i, 1).Text & "#") Then
'Find the text that we will be replacing the placeholder keyword with
Set valueSrc = Range(Mid(DocGenKeys.Cells(i, 2).Formula, 2))
If valueSrc.MergeCells Then value = valueSrc.MergeArea.Cells(1, 1).Text Else value = valueSrc.Text
'Perform the replacement
WordDocReplace oDoc, "#" & DocGenKeys.Cells(i, 1).Text & "#", value
End If
Next i
End Sub
это функция, используемая для определения, существует ли ключевое слово в документе:
' Purpose: Function called for each replacement to first determine as quickly as possible whether
' the document contains the keyword, and thus whether replacement actions must be taken.
Public Function WordDocContains(ByRef oDoc As Word.Document, ByVal searchFor As String) As Boolean
Application.StatusBar = "Checking for keyword: " & searchFor
WordDocContains = False
Dim storyRange As Word.Range
For Each storyRange In oDoc.StoryRanges
With storyRange.Find
.Text = searchFor
WordDocContains = WordDocContains Or .Execute
End With
If WordDocContains Then Exit For
Next
End Function
и именно здесь резина встречается с дорогой - кодом, который выполняет замену. Эта процедура усложнялась по мере того, как я сталкивался с трудностями. Вот уроки, которые вы узнаете только из опыта:
вы можете установить текст замены напрямую, или вы можете использовать буфер обмена. Я узнал, что если вы делаете замену VBA в word, используя строка длиной более 255 символов, текст будет усечен, если вы попытаетесь поместить его в
Find.Replacement.Text
, но вы можете использовать"^c"
в качестве замены текста, и он получит его непосредственно из буфера обмена. Это был обходной путь, который я должен был использовать.просто вызов replace пропустит ключевые слова в некоторых текстовых областях, таких как верхние и нижние колонтитулы. Из-за этого вам действительно нужно перебирать
document.StoryRanges
и запустите поиск и замените на каждом из них, чтобы убедиться, что вы поймаете все экземпляры слова, которое вы хотите заменить.если вы
Replacement.Text
непосредственно вам нужно преобразовать разрывы строк Excel (vbNewLine
иChr(10)
) С простымvbCr
чтобы они правильно отображались в word. В противном случае, везде, где ваш текст замены имеет разрывы строк, поступающие из ячейки excel, в конечном итоге вставит странные символы в word. Однако если вы используете метод буфера обмена, вам не нужно этого делать, так как разрывы строк преобразуются автоматически когда положить в буфер обмена.
это все объясняет. Комментарии тоже должны быть достаточно ясными. Вот золотая процедура, которая выполняет магию:
' Purpose: This function actually performs replacements using the Microsoft Word API
Public Sub WordDocReplace(ByRef oDoc As Word.Document, ByVal replaceMe As String, ByVal replaceWith As String)
Dim clipBoard As New MSForms.DataObject
Dim storyRange As Word.Range
Dim tooLong As Boolean
Application.StatusBar = "Replacing instances of keyword: " & replaceMe
'We want to use regular search and replace if we can. It's faster and preserves the formatting that
'the keyword being replaced held (like bold). If the string is longer than 255 chars though, the
'standard replace method doesn't work, and so we must use the clipboard method (^c special character),
'which does not preserve formatting. This is alright for schedules though, which are always plain text.
If Len(replaceWith) > 255 Then tooLong = True
If tooLong Then
clipBoard.SetText IIf(replaceWith = vbNullString, "", replaceWith)
clipBoard.PutInClipboard
Else
'Convert excel in-cell line breaks to word line breaks. (Not necessary if using clipboard)
replaceWith = Replace(replaceWith, vbNewLine, vbCr)
replaceWith = Replace(replaceWith, Chr(10), vbCr)
End If
'Replacement must be done on multiple 'StoryRanges'. Unfortunately, simply calling replace will miss
'keywords in some text areas like headers and footers.
For Each storyRange In oDoc.StoryRanges
Do
With storyRange.Find
.MatchWildcards = True
.Text = replaceMe
.Replacement.Text = IIf(tooLong, "^c", replaceWith)
.Wrap = wdFindContinue
.Execute Replace:=wdReplaceAll
End With
On Error Resume Next
Set storyRange = storyRange.NextStoryRange
On Error GoTo 0
Loop While Not storyRange Is Nothing
Next
If tooLong Then clipBoard.SetText ""
If tooLong Then clipBoard.PutInClipboard
End Sub
когда пыль оседает, мы остаемся с красивой версией исходного документа с производственными значениями вместо этих ключевых слов, помеченных хэшем. Я хотел бы показать пример, но, конечно, каждый заполненный документ содержит всю проприетарную информацию.
только думаю, осталось упомянуть, что я думаю, что . Он делает что - то очень похожее-он в конечном итоге вызывает то же самое WordDocReplace
функция, но что особенного в ключевых словах, используемых здесь, заключается в том, что они не связаны с одной ячейкой в исходной книге, они генерируются в коде из списков в книге. Так, например, одна из усовершенствованных замен будет выглядеть следующим образом:
'Generate the schedule of vessels
If WordDocContains(oDoc, "#VESSELSCHEDULE#") Then _
WordDocReplace oDoc, "#VESSELSCHEDULE#", GenerateVesselSchedule()
и тогда будет соответствующая процедура, которая объединяет строка, содержащая всю информацию о судне, настроенную пользователем:
' Purpose: Generates the list of vessels from the "Vessels" sheet based on the user's configuration
' in the booking tab. The user has the option to generate one or both of Owned Vessels
' and Chartered Vessels, as well as what fields to display. Uses a helper function.
Public Function GenerateVesselSchedule() As String
Dim value As String
Application.StatusBar = "Generating Schedule of Vessels."
If Booking.Range("ListVessels").value = "Yes" Then
Dim VesselCount As Long
If Booking.Range("ListVessels").Offset(1).value = "Yes" Then _
value = value & GenerateVesselScheduleHelper("Vessels", VesselCount)
If Booking.Range("ListVessels").Offset(1).value = "Yes" And _
Booking.Range("ListVessels").Offset(2).value = "Yes" Then _
value = value & "(Chartered Vessels)" & vbNewLine
If Booking.Range("ListVessels").Offset(2).value = "Yes" Then _
value = value & GenerateVesselScheduleHelper("CharteredVessels", VesselCount)
If Len(value) > 2 Then value = Left(value, Len(value) - 2) 'Remove the trailing line break
Else
GenerateVesselSchedule = Booking.Range("VesselSchedAlternateText").Text
End If
GenerateVesselSchedule = value
End Function
' Purpose: Helper function for the Vessel Schedule generation routine. Generates either the Owned or
' Chartered vessels based on the schedule parameter passed. The list is numbered and contains
' the information selected by the user on the Booking sheet.
' SENSITIVE: Note that this routine is sensitive to the layout of the Vessel Schedule tab and the
' parameters on the Configure Quotes tab. If either changes, it should be revisited.
Public Function GenerateVesselScheduleHelper(ByVal schedule As String, ByRef VesselCount As Long) As String
Dim value As String, nextline As String
Dim numInfo As Long, iRow As Long, iCol As Long
Dim Inclusions() As Boolean, Columns() As Long
'Gather info about vessel info to display in the schedule
With Booking.Range("VesselInfoToInclude")
numInfo = Booking.Range(.Cells(1, 1), .End(xlToRight)).Columns.Count - 1
ReDim Inclusions(1 To numInfo)
ReDim Columns(1 To numInfo)
On Error Resume Next 'Some columns won't be identified
For iCol = 1 To numInfo
Inclusions(iCol) = .Offset(0, iCol) = "Yes"
Columns(iCol) = sumSchedVessels.Range(schedule).Cells(1).EntireRow.Find(.Offset(-1, iCol)).Column
Next iCol
On Error GoTo 0
End With
'Build the schedule
With sumSchedVessels.Range(schedule)
For iRow = .row + 1 To .row + .Rows.Count - 1
If Len(sumSchedVessels.Cells(iRow, Columns(1)).value) > 0 Then
VesselCount = VesselCount + 1
value = value & VesselCount & "." & vbTab
nextline = vbNullString
'Add each property that was included to the description string
If Inclusions(1) Then nextline = nextline & sumSchedVessels.Cells(iRow, Columns(1)) & vbTab
If Inclusions(2) Then nextline = nextline & "Built: " & sumSchedVessels.Cells(iRow, Columns(2)) & vbTab
If Inclusions(3) Then nextline = nextline & "Length: " & _
Format(sumSchedVessels.Cells(iRow, Columns(3)), "#'") & vbTab
If Inclusions(4) Then nextline = nextline & "" & sumSchedVessels.Cells(iRow, Columns(4)) & vbTab
If Inclusions(5) Then nextline = nextline & "Hull Value: " & _
Format(sumSchedVessels.Cells(iRow, Columns(5)), "$#,##0") & vbTab
If Inclusions(6) Then nextline = nextline & "IV: " & _
Format(sumSchedVessels.Cells(iRow, Columns(6)), "$#,##0") & vbTab
If Inclusions(7) Then nextline = nextline & "TIV: " & _
Format(sumSchedVessels.Cells(iRow, Columns(7)), "$#,##0") & vbTab
If Inclusions(8) And schedule = "CharteredVessels" Then _
nextline = nextline & "Deductible: " & Format(bmCharterers.Range(schedule).Cells( _
iRow - .row, 9), "$#,##0") & vbTab
nextline = Left(nextline, Len(nextline) - 1) 'Remove the trailing tab
'If more than 4 properties were included insert a new line after the 4th one
Dim tabloc As Long: tabloc = 0
Dim counter As Long: counter = 0
Do
tabloc = tabloc + 1
tabloc = InStr(tabloc, nextline, vbTab)
If tabloc > 0 Then counter = counter + 1
Loop While tabloc > 0 And counter < 4
If counter = 4 Then nextline = Left(nextline, tabloc - 1) & vbNewLine & Mid(nextline, tabloc)
value = value & nextline & vbNewLine
End If
Next iRow
End With
GenerateVesselScheduleHelper = value
End Function
полученную строку можно использовать так же, как содержимое любой ячейки excel, и передать функции замены, которая будет соответствующим образом использовать метод буфера обмена, если он превышает 255 символов.
вот этот шаблон:
плюс эта таблица данные:
будет этого документа:
я искренне надеюсь, что это помогает кто-нибудь. Это определенно было огромное предприятие и сложное колесо, которое нужно было заново изобрести. Приложение огромно, с более чем 50 000 строк кода VBA, поэтому, если я ссылался на важный метод в моем коде где-то, что кому-то нужно, пожалуйста, оставьте комментарий, и я добавлю его здесь.
http://www.computorcompanion.com/LPMArticle.asp?ID=224 описывает использование слова закладки
фрагмент текста в документе может быть закладки, и дано имя переменной. С помощью VBA эта переменная может быть доступна, и содержимое в документе может быть заменено альтернативным содержимым. Это решение для наличия заполнителей, таких как имя и адрес в документе.
кроме того, используя закладки, документы может быть изменен на текст с закладками. Если имя появляется несколько раз в документе, первый экземпляр может быть помечен закладкой, а дополнительные экземпляры могут ссылаться на закладку. Теперь, когда первый экземпляр программно изменен, все остальные экземпляры переменной по всему документу также автоматически изменяются.
теперь все, что нужно, это обновить все документы путем закладки текста заполнителя и использования согласованного соглашения об именах во всем документы, затем повторите каждый документ, заменив закладку, если она существует:
document.Bookmarks("myBookmark").Range.Text = "Inserted Text"
Я, вероятно, могу решить проблему переменных, которые не отображаются в данном документе, используя предложение on error resume next перед попыткой каждой замены.
спасибо Даг Глансе за упоминание о существовании закладок в своем комментарии. Я не знал об их существовании заранее. Я буду держать эту тему в курсе этого решения хватает.
вы можете рассмотреть подход на основе XML.
Word имеет функцию, называемую пользовательской привязкой данных XML или элементами управления контентом с привязкой данных. Элемент управления содержимым-это, по сути, точка в документе, которая может содержать содержимое. Элемент управления содержимым "с привязкой к данным" получает содержимое из XML-документа, включенного в zip-файл docx. Выражение XPath используется, чтобы сказать, какой бит XML. Поэтому все, что вам нужно сделать, это включить XML-файл, а Word сделает все остальное.
в Excel способы получения данных из него в виде XML, поэтому все решение должно работать хорошо.
существует много информации о привязке данных управления контентом к MSDN (некоторые из которых упоминались в предыдущих вопросах SO), поэтому я не буду их включать здесь.
но вам нужен способ настройки Привязок. Вы можете использовать Content Control Toolkit, или если вы хотите сделать это из Word, моя надстройка OpenDoPE.
выполнив аналогичную задачу, я обнаружил, что вставка значений в таблицы была намного быстрее, чем поиск именованных тегов - данные могут быть вставлены следующим образом:
With oDoc.Tables(5)
For i = 0 To Data.InvoiceDictionary.Count - 1
If i > 0 Then
oDoc.Tables(5).rows.Add
End If
Set invoice = Data.InvoiceDictionary.Items(i)
.Cell(i + 2, 1).Range.Text = invoice.InvoiceCCNumber
.Cell(i + 2, 2).Range.Text = invoice.InvoiceDate
.Cell(i + 2, 3).Range.Text = invoice.TransactionType
.Cell(i + 2, 4).Range.Text = invoice.Description
.Cell(i + 2, 5).Range.Text = invoice.SumOfValue
Next i
.Ячейки(i + 1, 4).Диапазон.Text = " Всего:" Конец С в этом случае строка 1 таблицы была заголовками; строка 2 была пуста, и больше не было строк-таким образом, строк.add применяется один раз больше, чем одна строка была присоединена. Таблицы могут быть очень подробными документами, и, скрывая границы и границы ячеек, можно чтобы выглядеть как обычный текст. Таблицы нумеруются последовательно в соответствии с потоком документов. (т. е.Таблицы (1) - первая таблица...