自从我提出这个问题以来已经很久了,我的解决方案越来越精细。我不得不处理各种特殊情况,例如直接来自工作簿的值、需要根据列表特别生成的部分,以及在页眉和页脚中进行替换的需求。
事实证明,使用书签是不够的,因为用户可以后来编辑文档,改变、添加和删除文档中的占位符值。解决方案实际上是使用关键字,如下所示:
![enter image description here](https://istack.dev59.com/umOzP.webp)
这只是一个示例文档中的页面,其中使用了一些可能自动插入文档的值。存在超过50个完全不同结构和布局的文档,并使用不同的参数。 Word文档和Excel电子表格唯一共享的知识是这些占位符值代表什么。在Excel中,这存储在文档生成关键字列表中,其中包含关键字,后跟实际包含此值的范围的引用:
![enter image description here](https://istack.dev59.com/4yXqD.webp)
这是所需的两个关键因素。现在,通过一些巧妙的代码,我只需要迭代要生成的每个文档,然后迭代所有已知关键字的范围,并在每个文档中为每个关键字进行搜索和替换。
首先,我有一个包装器方法,它负责维护 Microsoft Word 实例,迭代选定用于生成的所有文档,对文档进行编号,并处理用户界面(例如处理错误、向用户显示文件夹等)。
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."
srcPath = FindConstant("Document Repository")
GetNextEndorsementNumber reset:=True
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
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
来打开文档、准备快速替换的环境、完成后更新链接、处理错误等操作。
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
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
如果文档包含我们的关键字。请注意,尝试查找一堆单词以确定它们不存在比不加选择地调用替换要快得多,因此我们始终在尝试替换之前检查关键字是否存在。
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
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
WordDocReplace oDoc, "#" & DocGenKeys.Cells(i, 1).Text & "#", value
End If
Next i
End Sub
这是用于检测文档中是否存在关键词的函数:
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
这就是关键所在——执行替换的代码。随着困难的出现,这个过程变得越来越复杂。下面是你只能通过经验学到的教训:
您可以直接设置替换文本,或者使用剪贴板。我发现如果您在Word中使用长于255个字符的字符串进行VBA替换,则如果您尝试将其放置在Find.Replacement.Text
中,文本将被截断,但您可以使用"^c"
作为替换文本,它将直接从剪贴板获取。这是我使用的解决方法。
仅调用替换将错过某些文本区域(如页眉和页脚)中的关键字。因此,您实际上需要遍历document.StoryRanges
并对每个范围运行搜索和替换,以确保捕获要替换的所有单词实例。
如果您直接设置Replacement.Text
,则需要使用简单的vbCr
将Excel换行符(vbNewLine
和Chr(10)
)转换为它们在Word中正确显示。否则,您的替换文本中任何来自Excel单元格的换行都会在Word中插入奇怪的符号。然而,如果您使用剪贴板方法,则不需要执行此操作,因为当放入剪贴板时,换行符会自动转换。
这解释了一切。评论应该也很清晰。以下是执行魔法的黄金例程:
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
If Len(replaceWith) > 255 Then tooLong = True
If tooLong Then
clipBoard.SetText IIf(replaceWith = vbNullString, "", replaceWith)
clipBoard.PutInClipboard
Else
replaceWith = Replace(replaceWith, vbNewLine, vbCr)
replaceWith = Replace(replaceWith, Chr(10), vbCr)
End If
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
当灰尘落定时,我们将得到一份美丽的初始文档版本,其中生产价值取代了那些标记为哈希的关键词。我很想展示一个例子,但当然每个填写好的文档都包含了所有专有信息。
我想唯一需要提到的就是
RunAdvancedReplacements
部分了。它做的事情非常相似——最终会调用相同的
WordDocReplace
函数,但这里使用的关键字非常特殊,它们不与原始工作簿中的单个单元格链接,而是从工作簿中的列表中在后台代码中生成。因此,例如,高级替换之一看起来像这样:
If WordDocContains(oDoc, "#VESSELSCHEDULE#") Then _
WordDocReplace oDoc, "#VESSELSCHEDULE#", GenerateVesselSchedule()
然后将有对应的例程,根据用户配置的所有船只信息组合成一个字符串:
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)
Else
GenerateVesselSchedule = Booking.Range("VesselSchedAlternateText").Text
End If
GenerateVesselSchedule = value
End Function
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
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
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
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
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)
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个字符,将适当地使用剪贴板方法。
因此,这个模板:
![enter image description here](https://istack.dev59.com/uCAiE.webp)
加上这个电子表格数据:
![enter image description here](https://istack.dev59.com/wBtj3.webp)
变成这个文档:
![enter image description here](https://istack.dev59.com/oS1du.webp)
我真诚地希望这篇文章能够在某一天对某个人有所帮助。这绝对是一项巨大的工程和一个复杂的轮子需要重新发明。该应用程序很大,有超过50,000行的VBA代码,因此如果我在我的代码中引用了某个关键方法,有人需要,请留下评论,我会在这里添加它。