Générer des documents Word (en Excel VBA) à partir d'une série de gabarits de documents

salut à tous. Je vais essayer de faire cette brève et simple. :)

j'ai

  1. 40 standard des documents word avec une série de champs (Nom, adresse, etc) qui doivent être remplis. Cela se fait traditionnellement manuellement, mais c'est répétitif et encombrant.
  2. Un classeur où un utilisateur a rempli d'un vaste ensemble d'informations sur un individu.

j'ai besoin de

  • une façon de programmatically (de Excel VBA) ouvrir ces documents boilerplate, éditer dans la valeur des champs de diverses gammes nommées dans le classeur, et enregistrer les gabarits remplis dans un dossier local.

si J'utilisais VBA pour éditer programmatiquement des valeurs particulières dans un ensemble de tableurs, j'éditerais tous ces tableurs pour contenir un ensemble de plages nommées qui pourraient être utilisées pendant le processus d'auto-remplissage, mais je ne suis pas au courant d'aucune fonctionnalité de 'champ nommé' dans un mot document.

Comment puis-je éditer les documents, et créer une routine VBA, de sorte que je puisse ouvrir chaque document, chercher un ensemble de champs qui pourraient devoir être remplis, et substituer une valeur?

Par exemple, quelque chose qui fonctionne comme ceci:

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

les Choses que j'ai vu:

  • mail fusionner - mais c'est insuffisant parce qu'il exige l'ouverture manuelle de chaque document et la structuration du classeur comme source de données, je veux en quelque sorte le opposé. Les modèles sont la source de données et le cahier de travail est itératif à travers eux. En outre, Mail merge est pour créer de nombreux documents identiques en utilisant une table de données différentes. J'ai beaucoup de documents qui utilisent les mêmes données.
  • utiliser un texte comme "#NAME# " et ouvrir chaque document pour une recherche et remplacer. C'est la solution à laquelle je recourrai si rien de plus élégant n'est proposé.
19
demandé sur Community 2011-02-24 18:30:59

4 réponses

Cela fait longtemps que je n'ai pas posé cette question, et ma solution a été de plus en plus raffinée. J'ai eu à faire face à toutes sortes de cas spéciaux, tels que les valeurs qui viennent directement du cahier de travail, les sections qui doivent être spécialement générées sur la base de listes, et la nécessité de faire des remplacements dans les en-têtes et les pieds de page.

comme il s'avère, il ne suffisait pas d'utiliser des signets, car il était possible pour les utilisateurs de modifier plus tard des documents pour changer, ajouter et supprimer valeurs placeholder des documents. La solution était en fait d'utiliser mots-clés comme ceci:

enter image description here

il s'agit simplement d'une page d'un document type qui utilise certaines des valeurs possibles qui peuvent être insérées automatiquement dans un document. Plus de 50 documents existent avec des structures et des dispositions complètement différentes, et en utilisant des paramètres différents. La seule connaissance commune partagée par les documents word et la feuille de calcul excel est la connaissance de ce que ces valeurs d'espace réservé sont censés représenter. Dans excel, cela est stocké dans une liste de mots clés de génération de document, qui contiennent le mot clé, suivi d'une référence à la gamme qui contient réellement cette valeur:

enter image description here

ce sont les deux ingrédients clés nécessaires. Maintenant avec quelque code intelligent, tout ce que j'avais à faire était itérer sur chaque document à générer, puis itérer sur la gamme de tous les mots clés connus, et faire une recherche et remplacer pour chaque mot clé dans chaque document.


tout d'abord, j'ai la méthode wrapper, qui prend soin de maintenir une instance de microsoft word itérant sur tous les documents sélectionnés pour la génération, la numérotation des documents, et faire les choses de l'interface utilisateur (comme les erreurs de manipulation, l'affichage du dossier à l'utilisateur, etc.)

' 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

Que des appels de routine RunReplacements qui s'occupe d'ouvrir le document, préparant l'environnement pour un fast le remplacement, la mise à jour des liens une fois cela fait, les erreurs de manipulation, etc:

' 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

Que la routine invoque alors RunSimpleReplacements. et RunAdvancedReplacements. Dans le premier, nous itérons sur l'ensemble des mots-clés de génération de Document et appel WordDocReplace si le document contient notre mot-clé. Notez qu'il est beaucoup plus rapide à essayer et Find un tas de mots pour comprendre qu'ils n'existent pas, puis pour appeler remplacer sans discernement, donc nous vérifions toujours si un mot-clé existe avant de tenter de remplacer il.

' 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

C'est la fonction utilisée pour détecter si un mot-clé existe dans le document:

' 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

Et c'est là où le caoutchouc rencontre la route - le code qui exécute le remplacement. Cette routine est devenue plus compliquée que j'ai rencontré des difficultés. Voici les leçons que vous apprendrez seulement de l'expérience:

  1. Vous pouvez définir le texte de remplacement directement, ou vous pouvez utiliser le presse-papiers. J'ai découvert la manière dure que si vous font un remplacement VBA dans word en utilisant une chaîne de caractères plus de 255 caractères, le texte sera tronqué si vous essayez de le placer dans le Find.Replacement.Text, mais vous pouvez utiliser "^c" comme votre texte de remplacement, et il l'obtiendra directement à partir du bloc-notes. C'était la solution de contournement j'ai eu à utiliser.

  2. simplement appeler replace manquera des mots-clés dans certaines zones de texte comme les en-têtes et les pieds de page. Pour cette raison, vous devez réellement itérer sur le document.StoryRanges et lancer la recherche et remplacer sur chacun d'eux pour s'assurer que vous attrapez toutes les instances du mot que vous souhaitez remplacer.

  3. si vous définissez le Replacement.Text directement, vous devez convertir Excel line breaks (vbNewLine et Chr(10)) avec un simple vbCr pour qu'ils apparaissent correctement dans word. Autrement, n'importe où votre texte de remplacement a des sauts de ligne venant d'une cellule excel finira par insérer des symboles étranges dans word. Si vous utilisez le presse-papiers méthode cependant, vous n'avez pas besoin de faire cela, comme les sauts de ligne sont convertis automatiquement lorsqu'ils sont mis dans le presse-papiers.

cela explique tout. Commentaires devrait être assez clair. Voici la routine d'or qui exécute la magie:

' 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

quand la poussière retombe, il nous reste une belle version du document initial avec des valeurs de production à la place de ces mots-clés Marqués de hachage. J'aimerais montrer un exemple, mais bien sûr, chaque document rempli contient tout-propriétaire information.


La seule chose que la gauche mentionner que je dirais que RunAdvancedReplacements section. Il fait quelque chose extrêmement similaire - il finit par appeler le même WordDocReplace fonction, mais ce qui est spécial à propos des mots-clés utilisés ici est qu'ils ne sont pas liés à une seule cellule dans le classeur original, ils sont générés dans le code-derrière des listes dans le classeur. Ainsi, par exemple, l'un des remplacements avancés ressemblerait à ceci:

'Generate the schedule of vessels
If WordDocContains(oDoc, "#VESSELSCHEDULE#") Then _
    WordDocReplace oDoc, "#VESSELSCHEDULE#", GenerateVesselSchedule()

Et puis il y aura une routine correspondante qui rassemble une chaîne contenant toutes les informations du vaisseau telles que configurées par l'utilisateur:

' 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

la chaîne résultante peut être utilisée comme le contenu de n'importe quelle cellule excel, et passée à la fonction de remplacement, qui utilisera de manière appropriée la méthode de bloc-notes si elle dépasse 255 caractères.

alors ce modèle:

enter image description here

les Plus de cette données de feuille de calcul:

enter image description here

Devient ce document:

enter image description here


j'espère sincèrement que cela aide quelqu'un un jour. C'était certainement une énorme entreprise et une roue complexe à réinventer. L'application est énorme, avec plus de 50.000 lignes de code VBA, donc si j'ai fait référence à une méthode cruciale dans mon code quelque part que quelqu'un a besoin, s'il vous plaît laisser un commentaire et je vais l'ajouter ici.

28
répondu Alain 2014-07-08 20:39:25

http://www.computorcompanion.com/LPMArticle.asp?ID=224 Décrit l'utilisation du Mot favoris

Une section de texte dans un document peut être signet, et donné un nom variable. VBA permet d'accéder à cette variable et de remplacer le contenu du document par un contenu alternatif. C'est une solution pour avoir des espaces réservés tels que le nom et L'adresse dans le document.

de plus, en utilisant des signets, des documents peut être modifié pour faire référence à un signet texte. Si un nom apparaît plusieurs fois dans un document, la première instance peut faire l'objet d'un signet, et d'autres instances peuvent faire référence au signet. Maintenant, lorsque la première instance est programmatiquement changée, toutes les autres instances de la variable dans le document sont aussi changées automatiquement.

maintenant, tout ce qui est nécessaire est de mettre à jour tous les documents en signant le texte du placeholder et en utilisant une convention de nommage cohérente tout au long les documents, puis itérer sur chacun des documents de remplacer le signet si elle existe:

document.Bookmarks("myBookmark").Range.Text = "Inserted Text"

je peux probablement résoudre le problème des variables qui n'apparaissent pas dans un document donné en utilisant la clause On error resume next avant d'essayer chaque remplacement.

merci à Doug Glancy pour avoir mentionné l'existence de signets dans son commentaire. Je n'avais pas connaissance de leur existence avant. Je vais garder ce sujet posté sur cette solution suffire.

3
répondu Alain 2017-05-23 12:26:12

vous pourriez envisager une approche basée sur XML.

Word a une fonctionnalité appelée personnalisation de la liaison de données XML, ou contrôles de contenu liés aux données. Un contrôle de contenu est essentiellement un point du document qui peut contenir du contenu. Un contrôle de contenu" lié aux données " tire son contenu d'un document XML que vous incluez dans le fichier zip docx. Une expression XPath est utilisée pour dire quel morceau de XML. Donc tout ce que vous avez à faire est d'inclure votre fichier XML, et Word fera le reste.

Excel a les moyens d'obtenir des données hors de lui comme XML, de sorte que l'ensemble de la solution devrait fonctionner bien.

il y a beaucoup d'informations sur la liaison des données de contrôle de contenu sur MSDN (dont certaines ont été référencées dans des questions précédentes de SO) donc je ne vais pas prendre la peine de les inclure ici.

mais vous avez besoin d'un moyen de configurer les fixations. Vous pouvez utiliser la boîte à outils de contrôle de contenu, ou si vous voulez le faire à partir de Word, mon add-in OpenDoPE.

2
répondu JasonPlutext 2011-02-24 23:06:27

ayant fait une tâche similaire, j'ai trouvé que l'insertion de valeurs dans les tables était beaucoup plus rapide que la recherche de tags nommés - les données peuvent alors être insérées comme ceci:

    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

.De la cellule(i + 1, 4).Gamme.Texte = " Total:" Fin Avec dans ce cas, la rangée 1 du tableau était les en - têtes; la rangée 2 était vide et il n'y avait pas d'autres rangées-d'où les rangées.ajouter s'applique une fois que plus d'une rangée a été attachée. Les tableaux peuvent être des documents très détaillés et en cachant les frontières et les être fait pour ressembler au texte ordinaire. Les tableaux sont numérotés de façon séquentielle selon le cheminement du document. (c'est à dire Doc.Les tableaux(1) est la première table...

0
répondu Simon N 2016-03-18 16:22:25