Update all fields in a Word document when user types anything - vba

I want a way of updating all the fields on a document automatically. I currently have a macro which is linked to F9. This macro updates all the fields in the header and footer, as well as all the ones in the main document.
Sub UpdateFields()
Dim oStory As Range
For Each oStory In ActiveDocument.StoryRanges
oStory.Fields.Update
If oStory.StoryType <> wdMainTextStory Then
While Not (oStory.NextStoryRange Is Nothing)
Set oStory = oStory.NextStoryRange
oStory.Fields.Update
Wend
End If
Next oStory
Set oStory = Nothing
End Sub
Apart from setting the macro on every key, how would I make it so this macro runs when the user types anything?
For example a user may place a field in the footer or header which shows the amount of characters. If this was the case I would like to be able to see the characters field update as I type.

Here are the events in Word VBA :
For the application :
https://msdn.microsoft.com/EN-US/library/office/dn320473.aspx
For the document :
https://msdn.microsoft.com/EN-US/library/office/dn320613.aspx
I would suggest that you use the Application.WindowSelectionChange event (Occurs when the selection changes in the active document window) : https://msdn.microsoft.com/EN-US/library/office/ff192791.aspx
Public WithEvents appWord As Word.Application
Private Sub appWord_WindowSelectionChange(ByVal Sel As Selection)
UpdateFields
End Sub
And if you need more details on that, you'll find some here : https://msdn.microsoft.com/library/office/ff746018.aspx

Related

Word userform - only allowing to add data to bookmark once

I do have a userform in word which populates predefined bookmarks with values. I use following code to write the text to the bookmark:
Private Sub OKButton_Click()
Dim Text1 As Range
Set Text1 = ActiveDocument.Bookmarks("Text1").Range
Text1.Text = Me.ComboBox1.Value
When I hit the OK Button again, the text is added to the bookmark (and this can be done over and over again). This should not be possible. How can this be solved?
If the bookmark should only be written to once, I'd remove the bookmark when writing to it the first time. If the code runs again, this would cause an error, so check for the existence of the bookmark, first.
For example
Private Sub OKButton_Click()
Dim Text1 As Range
Dim doc As Document
Dim bkm As Bookmark
Dim bkmName As String
Set doc = ActiveDocument
bkmName = "Text1"
If doc.Bookmarks.exists(bkmName) Then
Set bkm = doc.Bookmarks(bkmName)
Set Text1 = bkm.Range
Text1.Text = "test" ' Me.ComboBox1.value
bkm.Delete
Else
Debug.Print "The bookmark has been removed."
End If
End Sub
Note that this approach assumes that the bookmark type is an "I-beam" type of bookmark: it marks a position in the document and contains no content.
If "bracket" bookmarks are used (the bookmark surrounds/contains at least one character) then the assigning text to the bookmark automatically deletes the bookmark. In that case, the line bkm.Delete is not required.
Usually, you would end an OK button sub with a command to close the userform, which discourages a second click:
Unload UserForm1
But if you want to make it impossible to add the text a second time, you would add a check of the existing text in the bookmark:
Private Sub OKButton_Click()
Dim Text1 As Range
Set Text1 = ActiveDocument.Bookmarks("Text1").Range
If Text1.Text <> Me.ComboBox1.value Then
Text1.Text = Me.ComboBox1.value
Else
MsgBox "That text has already been entered."
End If
End Sub

Select Range of DocProperty

I have endless Word-Documents, all with the same DocProperty somewhere in it.
Now I have to modify the font style of this specific DocProperty (e.g. make it bold), any other DocProperty has to been skipped.
How do I select this DocProperty with VBA?
I looked into the ActiveDocument.Range.Fields collection, but where is the name of the linked DocProperty? I'm only finding the Text, but that is the value of the actual CustomDocProperty
You were on the right track. In short you want: -
To look in Field.Type for a value of 85 (WdFieldDocProperty)
Then check the Field.Code for the property name/label
A sample of checking a document for it is below with comments to explain what is happening: -
Public Sub Sample()
Dim WdDoc As Word.Document
Dim Fld As Word.Field
'Connect to the Document
Set WdDoc = ThisDocument
'Only work if there are fields in the document to begin with
If WdDoc.Fields.Count > 0 Then
'Check each field
For Each Fld In WdDoc.Fields
'If the type is a DocProperty then we may have a match
If Fld.Type = wdFieldDocProperty Then
'If the code contains the name we are after then we have a match!
If InStr(1, Fld.Code, "Custom1") Then
'Select the field
Fld.Select
'Format the selection
Selection.Font.Bold = True
End If
End If
Next
End If
Set WdDoc = Nothing
End Sub
As a further tip (if you haven't done it all ready) would be to use the FileScriptingObject to help process all of your documents in one go using a loop. If you do try that and get stuck, start a new question with how far you got and what is not working and SO will help out I'm sure.

Macro to update all fields in a word document

I have built - over the years - a vba macro that is supposed to update all fields in a word document.
I invoke this macro before releasing the document for review to ensure all headers and footers etc are correct.
Currently - it look like this:
Sub UpdateAllFields()
'
' UpdateAllFields Macro
'
'
Dim doc As Document ' Pointer to Active Document
Dim wnd As Window ' Pointer to Document's Window
Dim lngMain As Long ' Main Pane Type Holder
Dim lngSplit As Long ' Split Type Holder
Dim lngActPane As Long ' ActivePane Number
Dim rngStory As Range ' Range Objwct for Looping through Stories
Dim TOC As TableOfContents ' Table of Contents Object
Dim TOA As TableOfAuthorities 'Table of Authorities Object
Dim TOF As TableOfFigures 'Table of Figures Object
Dim shp As Shape
' Set Objects
Set doc = ActiveDocument
Set wnd = doc.ActiveWindow
' get Active Pane Number
lngActPane = wnd.ActivePane.Index
' Hold View Type of Main pane
lngMain = wnd.Panes(1).View.Type
' Hold SplitSpecial
lngSplit = wnd.View.SplitSpecial
' Get Rid of any split
wnd.View.SplitSpecial = wdPaneNone
' Set View to Normal
wnd.View.Type = wdNormalView
' Loop through each story in doc to update
For Each rngStory In doc.StoryRanges
If rngStory.StoryType = wdCommentsStory Then
Application.DisplayAlerts = wdAlertsNone
' Update fields
rngStory.Fields.Update
Application.DisplayAlerts = wdAlertsAll
Else
' Update fields
rngStory.Fields.Update
If rngStory.StoryType <> wdMainTextStory Then
While Not (rngStory.NextStoryRange Is Nothing)
Set rngStory = rngStory.NextStoryRange
rngStory.Fields.Update
Wend
End If
End If
Next
For Each shp In doc.Shapes
If shp.Type <> msoPicture Then
With shp.TextFrame
If .HasText Then
shp.TextFrame.TextRange.Fields.Update
End If
End With
End If
Next
' Loop through TOC and update
For Each TOC In doc.TablesOfContents
TOC.Update
Next
' Loop through TOA and update
For Each TOA In doc.TablesOfAuthorities
TOA.Update
Next
' Loop through TOF and update
For Each TOF In doc.TablesOfFigures
TOF.Update
Next
' Header and footer too.
UpdateHeader
UpdateFooter
' Return Split to original state
wnd.View.SplitSpecial = lngSplit
' Return main pane to original state
wnd.Panes(1).View.Type = lngMain
' Active proper pane
wnd.Panes(lngActPane).Activate
' Close and release all pointers
Set wnd = Nothing
Set doc = Nothing
End Sub
Sub UpdateFooter()
Dim i As Integer
'exit if no document is open
If Documents.Count = 0 Then Exit Sub
Application.ScreenUpdating = False
'Get page count
i = ActiveDocument.BuiltInDocumentProperties(14)
If i >= 1 Then 'Update fields in Footer
For Each footer In ActiveDocument.Sections(ActiveDocument.Sections.Count).Footers()
footer.Range.Fields.Update
Next
End If
Application.ScreenUpdating = True
End Sub
'Update only the fields in your footer like:
Sub UpdateHeader()
Dim i As Integer
'exit if no document is open
If Documents.Count = 0 Then Exit Sub
Application.ScreenUpdating = False
'Get page count
i = ActiveDocument.BuiltInDocumentProperties(14)
If i >= 1 Then 'Update fields in Header
For Each header In ActiveDocument.Sections(ActiveDocument.Sections.Count).Headers()
header.Range.Fields.Update
Next
End If
Application.ScreenUpdating = True
End Sub
I have noticed recently that it sometimes misses some sections of the document. Today it missed First page footer -section 2- (the document version was not updated).
I have built this macro over a number of years and several bouts of research but I am not proud of it so please suggest a complete replacement if there is now a clean way of doing it. I am using Word 2007.
To test, create a word document and add a custom field named Version and give it a value. Then use that field {DOCPROPERTY Version \* MERGEFORMAT } in as many places as you can. Headers, Footers, first-page, subsequent page etc. etc. Remember to make a multi-section document with different header/footers. Then change the property and invoke the macro. It currently does quite a good job, handling TOCs and TOAs an TOFs etc, it just seems to skip footers (sometimes) in a multi-section document for example.
Edit
The challenging document that seems to cause the most problems is structured like this:
It has 3 sections.
Section 1 is for the title page and TOC so the first page of that section has no header/footer but does use the Version property on it. Subsequent pages have page numbering in roman numerals for the TOC.
Section 2 is for the body of the document and has headers and footers.
Section 3 is for the copyright blurb and this has a very strange header and a cut-down footer.
All footers contain the Version custom document property.
My code above seems to work in all cases except sometimes it misses first page footer of sections 2 and 3.
For years, the standard I've used for updating all fields (with the exception of TOC, etc. which are handled separately) in a document is the one the Word MVPs use and recommend, which I'll copy here. It comes from Greg Maxey's site: http://gregmaxey.mvps.org/word_tip_pages/word_fields.html. One thing it does that I don't see in your version is update any fields in Shapes (text boxes) in the header/footer.
Public Sub UpdateAllFields()
Dim rngStory As Word.Range
Dim lngJunk As Long
Dim oShp As Shape
lngJunk = ActiveDocument.Sections(1).Headers(1).Range.StoryType
For Each rngStory In ActiveDocument.StoryRanges
'Iterate through all linked stories
Do
On Error Resume Next
rngStory.Fields.Update
Select Case rngStory.StoryType
Case 6, 7, 8, 9, 10, 11
If rngStory.ShapeRange.Count > 0 Then
For Each oShp In rngStory.ShapeRange
If oShp.TextFrame.HasText Then
oShp.TextFrame.TextRange.Fields.Update
End If
Next
End If
Case Else
'Do Nothing
End Select
On Error GoTo 0
'Get next linked story (if any)
Set rngStory = rngStory.NextStoryRange
Loop Until rngStory Is Nothing
Next
End Sub
Some research and experimentation produced the following addition which seems to solve the additional problem of updating the headers/footers in a multi-section document.
Add the following dimensions to the earlier answer:
dim sctn as Word.Section
dim hdft as Word.HeaderFooter
And then, add to the earlier code
for each sctn in doc.Sections
for each hdft in sctn.Headers
hdft.Range.Fields.Update
next
for each hdft in sctn.Footers
hdft.Range.Fields.Update
next
next
However - I am still not happy with this code and would very much like to replace it with something less hacky.
Thanks for these answers! I found the answers very good and learned some stuff about ms-word macros. I thought I'd make my own answer for consideration (and adding some more search engine keywords - my searches didn't bring me here immediately).
I took inspiration from the citations in the footnotes.
I had an issue where MS Word fields were not updating in Textbox (Shapes).
I was working on a 70 page word document (Word 2013) that contained a lot of figures/images/captions and cross-references. A common practice is for an image to be captioned e.g. Figure 7, so it can be easily cross-referenced. Often the caption is inside a textbox (shape) and grouped with/to the object its captioning.
So after some document editing and content reorganisation, the fields and cross-references can easily get out of logical sequence.
OK - no problem... pressing CTRL+A then F9 to update the document fields should solve this?
Unfortunately that didn't work as expected to update fields in textboxes (shapes).
In this scenario where fields exist inside textboxes (shapes) CTRL+A then F9 only updated the fields not inside a textbox (shape).
One can assume this behaviour is because field updating (F9) works on selected text, and with the CTRL+A then F9 approach only text outside of the textboxes (shapes) is selected, so the field update only applies outside of textboxes (shapes).
I'm surprised there is not a button on the ribbon to perform an "update all fields". There could even be a toggle option to prompt the user to update all fields when closing a document?
I checked Word's (2013) ribbon command list, and didn't find an Update All command.
Solution UpdateAllFields()
Like the code shared by #Cindy here, the following code should update fields wherever they are in the doc, header, footer, main doc, textbox, grouped and nested grouped textbox.
Create a macro with the following code, and then add to the Quick Access Toolbar (QAT)
Press ALT+F8 to open the Macros dialogue.
Enter a name for the Macro: UpdateAllFields
Press Create button
Paste the code:
Sub UpdateAllFields()
Application.ScreenUpdating = False
With ActiveDocument
.Fields.Update
.PrintPreview
.ClosePrintPreview
End With
Application.ScreenUpdating = True
End Sub
Finally add the Macro to the Quick Access Toolbar.
Citations and inspirations:
The Q&A's in this post!
There is a related post on the Microsoft Community here: Word 365 Fields not updating in Textbox [serious reproducible error]. This suggests the issue is present in at least Word 2013 and Word 365.
There is a related post on Stack Overflow here: Macro to update fields in shapes (textboxes) in footer in Microsoft Word.
Another example UpdateTextboxFields()
This was the first version of code I wrote as I was in research and solution mode. Its a recursive approach to update fields inside textboxes, even if they are inside a group, or nested group. This doesn't update fields outside shapes.
Public Sub UpdateTextboxFields()
Application.ScreenUpdating = False
With ActiveDocument
Call IterateShapesCollection(.Shapes)
.PrintPreview
.ClosePrintPreview
End With
Application.ScreenUpdating = True
End Sub
Private Sub IterateShapesCollection(col)
Dim shp As Shape
For Each shp In col
' https://learn.microsoft.com/en-gb/office/vba/api/office.msoshapetype
' Ignore images and
If 1 = shp.Type Or 13 = shp.Type Then
GoTo NextIteration
End If
'Debug.Print ("Name: " & shp.Name & ", Type: " & shp.Type)
' if the type is a group, recurse
If 6 = shp.Type Then
Call IterateShapesCollection(shp.GroupItems)
Else
Call UpdateShapeFields(shp)
End If
NextIteration:
Next
End Sub
Private Sub UpdateShapeFields(shp)
With shp.TextFrame
If .HasText Then
.TextRange.Fields.Update
End If
End With
End Sub
Word display option: Update fields before printing
cite: Microsoft article Some fields are updated while other fields are not
The concept behind this option/approach is: all document fields are updated when you open print preview.
It looks like this option in Word (tested in 2013) updates all fields with a caveat - see below - you may need to open and close print preview twice.
File → Options → Display → Print options section → Update fields before printing
Caveat if the doc has cross-references to figures/captions
This caveat applies to the word "Update fields before printing" display option and the UpdateAllFields() macro.
IF the document contains cross-references to figures/captions (with numbers), and those figures/captions have changed sequence/place in the document...
You must update the fields twice, 1) to reflect the figures/captions update, and then 2) to update the cross-references.

Word VBA to control formatting and content of table cells in heading

Frequently, I have to change all headings of a word-document depending on a the content of a customproperty. For example, if the document prorperty 'Status' equeals to anything but final, all headings must contain 'DRAFT' written in bold characters on a red-background. If the document is 'final', DRAFT must not appear and the background must be 'none'.
Except for the first section, our headings contain a table consisting of one row and two cells. Cell 1 must be flipped depending on the 'Status', Cell 2 must remain unchanged.
Is there any known solution (VBA or cell-specific 'IF-THEN-ELSE Statements) to change the content of cell 1 throughout the whole document depending on the Status document property? Currently, I do have to scroll through all sections and change the header manually.
You can do this using a field and a macro. The field will create the values, and the macro will update them to account for changes.
Let's say that you're using the document property status as you've described. If the value is "DRAFT" the text in the table will be "This is a draft" and if it is anything else the text will be "This is not a draft". Word can be squirrely about these properties, so the first thing I would do is a test. Set your property to DRAFT and then create a field to ensure that Word is reading it.
Anywhere in your document type:
[Ctrl+F9] DOCPROPERTY Status
This will result in text that looks like
{DOCPROPERTY Status}
but be aware that you have to use Ctrl+F9 to get the special field brackets.
Now toggle the field code (select, right-click and choose Toggle Field Codes). If it becomes text that says DRAFT you are ready to go. If not, you may not be setting the property the way that Word wants you to. The way I do this is by going to Advanced Properties, clicking the Custom tab, finding Status in the list, adding the value, and clicking Add so that it appears below. There may be other ways, but that works.
Once you've had success with that field code, create a new one in your table that looks like this (remember that all brackets are created with Ctrl+F9):
{ IF { COMPARE { DOCPROPERTY Status } = "DRAFT" } = 1 "This is a draft" "This is not a draft" }
The If statement compares the value of the compare statement to 1, and the two quoted strings after reflect what will appear if the If statement evaluates to true, and what will appear if it evaluates to false. Toggle the field codes to see what you get.
Then, you can create a little macro that will update them all for you, so you don't have to manually update each one. Something like this should work:
Public Sub UpdateAllFields()
Dim objDoc as Document
Dim objSect As Section
Dim objHeader As HeaderFooter, objFooter As HeaderFooter
Set objDoc = ActiveDocument
objDoc.Fields.Update
For Each objSect In objDoc.Sections
For Each objHeader In objSect.Headers
objHeader.Range.Fields.Update
Next objHeader
For Each objFooter In objSect.Footers
objFooter.Range.Fields.Update
Next objFooter
Next objSect
End Sub
I misread and didn't realize the color change applied to these tables, so adding something about that.
The font color can be changed using the field. Change the field above so that it looks like this (remember about Ctrl+F9) (I'm adding some line breaks for readability. Do not include these in your field. Put it all on one line):
{ IF { COMPARE { DOCPROPERTY Status } = "DRAFT" } = 1
{ QUOTE "This is a draft" \*Charformat }
{ QUOTE "This is not a draft" \*Charformat } }
Then select each of the quote fields in turn and apply whatever font formatting you need. You can also apply highlighting in this way; I don't think this will be sufficient for your requirement to shade the whole cell but you might try it out and see if you can avoid additional steps.
If you definitely need to shade the whole cell, than you'll need another macro. Something like this should do it:
Sub ChangeCol()
Dim objDoc As Document
Dim objTable As Table, objCell As Cell
Dim objFld As Field
Set objDoc = ActiveDocument
For Each objFld In objDoc.Fields
If objFld.result.Information(wdWithInTable) = True And _
objFld.Code Like "*IF*" And _
objFld.Code Like "*DOCPROPERTY Status*" Then
If objDoc.CustomDocumentProperties("Status").Value = "DRAFT" Then
objFld.result.Cells(1).Shading.BackgroundPatternColor = wdColorRed
Else: objFld.result.Cells(1).Shading.BackgroundPatternColor = wdColorAutomatic
End If
End If
Next objFld
End Sub
Just run that along with the UpdateFields macro to keep them in sync (or write a third macro that triggers both of them so you don't forget).
Please accept my acknowledges once more. Hereafter, the macros I make use of for my purpose:
Private Sub colorizeTableCells(ByVal oFields As fields, sStatus As String)
Dim objFld As field
Dim bgColor As WdColor
oFields.Update
For Each objFld In oFields
If objFld.Result.Information(wdWithInTable) = True And _
objFld.Code Like "*IF*" And _
objFld.Code Like "*DOCPROPERTY Status*" Then
bgColor = wdColorAutomatic
If sStatus = "DRAFT" Then
bgColor = wdColorRed
End If
objFld.Result.Cells(1).Shading.BackgroundPatternColor = bgColor
End If
Next objFld
End Sub
Sub processHeaderAndFooterFields()
Dim objDoc As Document
Dim objSect As Section
Dim objHeader As HeaderFooter
Dim objFooter As HeaderFooter
Dim sStatus As String
Set objDoc = ActiveDocument
sStatus = objDoc.CustomDocumentProperties("Status").Value
For Each objSect In objDoc.Sections
For Each objHeader In objSect.Headers
colorizeTableCells oFields:=objHeader.range.fields, sStatus:=sStatus
Next objHeader
For Each objFooter In objSect.Footers
colorizeTableCells oFields:=objFooter.range.fields, sStatus:=sStatus
Next objFooter
Next objSect
End Sub

Return different values from a vba user form depending on the button pressed

I have been creating an acronym finding macro that will sit on a custom toolbar in word. When run it searches the document for acronyms and places them in a table. I want to include some user forms so that as the macro finds an acronym the user can select the predefined definition (got from an excel document) or enter their own new one (I know multiple acronyms meanings is frowned upon but it happens).
Anyway I am stuck. I have created a user form with three buttons. A text input and a label. Now I have managed to set the label text with the acronym that was found however I can't seem to get the buttons to change a variable, userChoice, and if applicable save the newly entered definition.
below is the test macro i have been trying this out on
Sub userFormTest()
Dim objExcel As Object
Dim objWbk As Object
Dim rngSearch As Object
Dim rngFound As Object
Dim targetCellValue As String
Dim userChoice As Integer
Set objDoc = ActiveDocument
Set objExcel = CreateObject("Excel.Application")
Set objWbk = objExcel.Workbooks.Open("C:\Users\Dave\Documents\Test_Definitions.xlsx")
objExcel.Visible = True
objWbk.Activate
With objWbk.Sheets("Sheet1")
Set rngSearch = .Range(.Range("A1"), .Range("A" & .Rows.Count).End(-4162))
Set rngFound = rngSearch.Find(What:="AA", After:=.Range("A1"), LookAt:=1)
If rngFound Is Nothing Then
UserForm1.Label1.Caption = "Acronym: AA" & vbCr & _
"Definition: Not found, please enter a definition below" & vbCr & _
" or choose to ignore this acronym"
UserForm1.Show
'an if statement here so that if the add button was pressed it adds to doc etc
Else
targetCellValue = .Cells(rngFound.Row, 2).Value
UserForm2.Label1.Caption = "Acronym: AA" & vbCr & _
"Definition: " & targetCellValue
UserForm2.Show
'an if statement here so that if the add button was pressed it adds to doc etc
End If
End With
objWbk.Close Saved = True
Set rngFound = Nothing
Set rngSearch = Nothing
Set objWbk = Nothing
objExcel.Visible = True
Set objExcel = Nothing
Set objDoc = Nothing
End Sub
I do realise that this could be done in the button_click() subs however I already have all the documents open etc in the other macro. Or is it possible to link to those already open documents? To be honest either way I would prefer to return to the main macro and just use the form to the user input.
I do realise that this could be done in the button_click() subs. However I already have all the documents open etc in the other macro. Or is it possible to link to those already open documents?
You can definitely link between button_click() subs and your main macro to modify the value of userChoice.
Answer:
What you need is some userform element (like a textbox) that can hold your value so you can refer back to it in your main macro. It looks like you already have this element (based upon your caption "Not found, please enter a definition below"). Let's say that element is a TextBox called Definition. Then let's say you want to return to the main macro after people push an "Add" button, as it appears you do (based upon your comment "so that if the add button was pressed it adds to doc").
In each of both Userform1 and Userform2, you would want something like this:
Private Sub AddButton_Click()
Userform1/Userform2.Hide
End Sub
That would return you to your main macro, where you could follow up with:
If Userform1/Userform2.Definition.Value = Whatever Then
'if the add button was pressed it adds to doc etc
End If
right where your existing comments are. Note that you could set userChoice = Userform1.Definition.Value here, but you don't need to because Userform1.Definition.Value already contains the information you need to track.
Additional material:
Rather than using the default instance of your Userform1 and Userform2 by using .Show on them immediately without assigning new instances of them to variables, may I suggest creating Userform variables to contain New instances of them like this:
Dim UnknownDefinition As Userform1
Set UnknownDefinition = New Userform1
UnknownDefinition.Show
Or if you want to get really optimal, you could follow more of the approach recommended here on how to make a properly instanced, abstracted userform:
Rubberduck VBA: How to create a properly instanced, abstracted userform
And now with bonus quotes from the post's author, #Mathieu Guindon:
make Definition a proper property, with the Property Let mutator changing the label value on top of changing its private backing field's value; avoid accessing form controls outside the form, treat them as private even if VBA makes them public.
Calling code wants data, not controls. You can extract properties/data, but not controls, into a dedicated model class.