Get Index of Specific Content Control based on ID or Title - vba

I would like to retrieve the word content control's index via VBA given a specific title without having to loop through all content controls looking for the title.
I know the title of the content control that I would like to select therefore I can set the ID of that specific content control to a variable for instance
a = ActiveDocument.SelectContentControlsByTitle("123").Item(1).ID
Now, I would like to know what the index of this item is among the other content controls in the document (over 450 content controls in the file template) such that I can refer to the content control index as a variable.
In lay terms I would like something along the lines of "b=getIndexOfA(a)" such that I can perform later process such as:
for i=b to ActiveDocument.ContentControls.Count
.....
next i
I am running Word 2016 on Windows 10.

The following approach works for just about object in the document body that's part of the text (as opposed to a floating image). Get a Range for the object, then set its starting point to the beginning of the document. Count all the objects of that type within the range:
Dim lIndexCC as Long
Dim cc as Word.ContentControl
Dim rng as Word.Range
Set cc = ActiveDocument.SelectContentControlsByTitle("123").Item(1)
Set rng = cc.Range
rng.Start = ActiveDocument.Content.Start
lIndexCC = rng.ContentControls.Count
Debug.Print lIndexCC

Related

Word VBA: Get table that is on the display screen at the moment

Suppose you have a microsoft word file (.DOCX), you open it and start to view it, using the mouse scrolling, from page 1 towards the last page.
Then, suppose you see a table, for instance, on page 4.
Now I ask: is it possible for Word-VBA to say to you what is the index or name of that table that is on the screen at the moment, no matter where is cursor located?
I want word-VBA fullfil a table that I am seeing at that moment, no matter where the cursor is located.
I hope I was clear enough...
Word doesn't have a direct way to get what's visible on-screen. It can be calculated, sort of, but not with 100% accuracy.
The following code sample does the trick for me, on my machine. It may need some tweaking to work on a different set up.
The object model does return the co-ordinates of the application window (ActiveWindow, here), the height of that Window and the UsableHeight - the height of the actual document working space. That can be used to get an estimated position.
There's also a Windows API function equivalent - RangeFromPoint - for a Window object that returns the a Range in the document for the given screen co-ordinates.
This code calculates a left and top position for the start of the visible part of the document, as well as for the end of the visible document. (In my test, it was a bit more, but not much). It then checks whether there is one or more tables within that scope. If it is, it takes the first one ( Set tbl = rngTargetStart.Tables(1)) - this returns the object your code needs to work with. As a "bonus", the code prints the index number of the table in the document and the page it's on to the Immediate Window.
Sub CheckForTableOnPage()
Dim WordWindowTop As Long 'in points
Dim WordWindowLeft As Long 'in points
Dim windowUsableHeight As Long 'in points
Dim rngTargetStart As Range
Dim rngTargetEnd As Range
Dim pageNumberTarget As Long
Dim tbl As Table
WordWindowTop = ActiveWindow.height
WordWindowLeft = ActiveWindow.left
windowUsableHeight = ActiveWindow.UsableHeight
RibbonFactor = 200
Set rngTargetStart = ActiveWindow.RangeFromPoint(WordWindowLeft, WordWindowTop - windowUsableHeight)
Set rngTargetEnd = ActiveWindow.RangeFromPoint(WordWindowLeft, WordWindowTop + windowUsableHeight)
rngTargetStart.End = rngTargetEnd.End
If rngTargetStart.Tables.Count >= 1 Then
pageNumberTarget = rngTargetStart.Information(wdActiveEndPageNumber)
Set tbl = rngTargetStart.Tables(1)
rngTargetStart.Start = ActiveDocument.Content.Start
Debug.Print "The table on page " & pageNumberTarget & " is number: " & rngTargetStart.Tables.Count
End If
End Sub

Populating a text box with some text depending on values of dropdowns selected in document

I am making a document in Microsoft Word 2016 and I would like this to be a form that a user will fill out (using Dropdown list content control)- from there I have assigned all of the items in the list with numbers for their values. I need to populate a text box with some words (determined by the sum of the values) and I am having trouble. Never used VBA- I don't even know if my first line is correct. I'm really not sure how to begin the document and how to populate the sum of all the dropdowns. I'm not using macros from another document, I just want to populate based upon what users select in the word document. I named my field with a tag on a text box that I want the text "account" "account 2" etc to appear in. Thanks!
Set myField = Selection.FormFields(1)
If myField.Type = wdFieldFormDropDown Then
Num = myField.DropDown.ListEntries.Count
If Num >= 75 Then
myField.Value = "Account 1"
End If
If Num > 50 Then
myField.Value = "Account 2"
End If
If Num <= 50 Then
myField.Value = "Account 3"
End If
End If
End Sub
If you plan to use Content Controls then FormField object in your code is not correct. Form fields are "legacy" - they're still quite useful, but it's a different part of the object model.
Content Controls are "embedded" in the document and have events to trigger code. That means you need to double-click the ThisDocument entry in the Project for the parent document in the VB Editor. Then select "Document" from the dropdown to the left of the Code Window and "ContentControlOnExit" from the dropdown on the right. That will insert the event stub in the code window for ThisDocument (see picture).
A Content Control event will trigger for all content controls in the document, which means you need to distinguish which content control triggered it. The event passes in a ContentControl object which can be tested for this purpose, usually using Select Case.
The sample code shows how to do this, how to get the count of the ListEntries, how to select a specific content control by its title (or by its tag) and how to write content to the content control. (Since what you want to do with Num in your code, above, is not clear to me I don't try to include that. But it looks like here, as well, you could use Select Case.)
Private Sub Document_ContentControlOnExit(ByVal ContentControl As ContentControl, _
Cancel As Boolean)
Dim cc As Word.ContentControl
Dim doc As Word.Document
Dim countListEntries As Long
Set doc = ContentControl.Parent
Select Case ContentControl.Title
Case "SelectAccount"
countListEntries = ContentControl.DropdownListEntries.Count
Set cc = doc.SelectContentControlsByTitle("Account").Item(1)
'Set cc =doc.SelectContentControlsByTag("Account").Item(1)
cc.Range.Text = ContentControl.Range.Text & " " & countListEntries
Case Else
End Select
End Sub

Protect custom properties in word

We have several templates in Word2016 which uses custom variables, these variables should be updated with my macro, so that data gets pushed to the Database when the user changes them.
Unfortunately we have some users that deletes the variables in the documents(in the text not in the file properties) which means that the database does not get updated. Is there a setting to make custom properties not able to be deleted from the text?
Properties for one example document are listed below
This is how it should look like in the document
Then this is what happens sometimes, which should not be allowed
By doing so I do not need to loop through document to find variables, because I can simply loop through custom properties:
Public Sub initializeVariablesFromDB(doc As Document, dokID As String)
Dim docProp As Object
Dim rowNumber As Integer
'Get valid properties
If CPearson.IsArrayEmpty(Settings.validPropertiesArray) Then
Settings.validPropertiesArray = Post.helpRequest("xxxxxx?Dok2=1")
End If
'We create the docid just in case....
Call CustomProperties.createCustomDocumentProperty(doc, "_DocumentID", dokID)
'We loop through all custom properties
For Each docProp In doc.CustomDocumentProperties
rowNumber = CPearson.findRownumber(docProp.name, Settings.validPropertiesArray)
If rowNumber <> -1 Then
'we clear all SIGN properties...
If InStr(UCase(docProp.name), "SIGN") > 0 Then
docProp.value = ""
End If
'We check if we should use value from DB
If Settings.validPropertiesArray(rowNumber, 1) = "0" Or InStr(UCase(docProp.name), "DOCUMENTCREATEDDATE") > 0 _
Or InStr(UCase(docProp.name), "DOCUMENTCREATOR") > 0 Or InStr(UCase(docProp.name), "DOCUMENTNAME") > 0 Then
'Update from DB
Call Post.updateDBFromCustomProperties(docId:=dokID, PropertyName:=docProp.name, whoRules:="dBrules", doc:=doc)
End If
End If
Next docProp
End Sub
This will fail for the _DocumentSubject in the text, the new value to the property will be collected but is not reflected in the text anymore because the user deleted the variable in the text, can I prevent this?
You can put the field into a Content Control and lock the content control. Users who know enough about MS Word can still delete the content control but they would need to take some fairly deliberate steps to do so (and I'm guessing those people are not the problem).
Normal fields, including document properties, can be added to content controls (probably best to use rich text).
You can put the field into a Content Control, seems like you can not do that :( – skatun 10 hours ago
You can totally do it, follow these steps (Word 2010):
Add a custom property to a new blank (even unsaved) document _SlowLearner
Add a RichText content control from the DeveloperTab
Click inside the content control
Insert > Quick Parts > Field
Filter fields Document Information > select DocProperty
Select property: _SlowLearner > OK
Note: the field is protected when the Content Control is fully locked. This has the curious side effect that the Content Control does NOT allow the field to update normally... to get around this you need to add some more code to your VBA, like this:
Sub UpdateProtectedField()
With ActiveDocument.ContentControls(1)
.LockContents = False
.Range.Fields(1).Update
.LockContents = True
End With
End Sub
One possibility is to use continuous sections breaks and protect sections, not an ideal solution because then images can not be formatted and so on..

Counting words in Word document, including footnores

I periodically receive long documents that include footnotes and am trying to find a way using VBA to count the number of words on each page, including footnotes. It doesn't matter if a footnote spills over onto the next page, I just the word count including footnotes that are anchored on the page.
I have a macro that correctly counts the number of words in the body of the text, using the command:
WordCount = ActiveDocument.Range(Start:=pos1, End:=pos2).ComputeStatistics(wdStatisticWords)
The variables pos1 and pos2 have been set to the first and last characters of the page being counted.
However, when I add the True parameter to ComputeStatistics(wdStatisticWords, True), to IncludeFootnotesAndEndnotes, as in:
WordCount = ActiveDocument.Range(Start:=pos1, End:=pos2).ComputeStatistics(wdStatisticWords, True)
it doesn't work, giving an error that there are too many parameters. It appears that when using a Range, the IncludeFootnotesAndEndnotes parameter is not available.
How do you count the words within footnotes contained in a range?
I think what you will need to do is iterate into each of the StoryRanges and update a counter. Here is a small example that should serve as an example, however, you will likely need to tweak it for your specific case (review my note about the enum for StoryRanges)
Here's the code:
Public Sub Count_All_Words()
Dim Story_Ranges As Variant: Set Story_Ranges = ActiveDocument.StoryRanges
Dim Story_Range As Object
Dim WordCount As Long
'Loop through each story range and only include the footer and Main story to the word count
For Each Story_Range In Story_Ranges
'You may need to check additional types, lookup the enumerations for StoryType here:
'https://msdn.microsoft.com/en-us/library/bb238219(v=office.12).aspx
If Story_Range.StoryType = wdMainTextStory Or Story_Range.StoryType = wdFootnoteSeparatorStory Then
'Add to the word count
WordCount = WordCount + Story_Range.ComputeStatistics(wdStatisticWords)
End If
Next
Debug.Print "The word count is: " & WordCount
End Sub

Insert Table from Builiding Block in specified location in the word document.

I want to insert a table which is defined as building block. I placed a content control in specified location in the document and refer to it by "selectcontetcontrolsbytag". Unfortunetly when table is inserted to the conentcontrol, it is convertered to RichText. Here is my code:
ThisDocument.SelectContentControlsByTag("TermsConditions").Item(1).Range = _
ActiveDocument.AttachedTemplate.BuildingBlockTypes.Item(wdTypeTables).Categories.Item("Terms and Conditions Translation").BuildingBlocks.Item("Terms and Conditions Eng")
Could you help me with proper code to insert building block in specified location. Also I would like this building block to be replaced by another, when user will select other item from userform, combobox etc.
Complete solution for my problem is:
Solution proposed by Cindy Meister Replacing content inside content
control:
To change content inside content control "TermsConditions" I added following code:
If doc.SelectContentControlsByTag("TermsConditions").Item(1).Range.Text <> doc.SelectContentControlsByTag("TermsConditions").Item(1).PlaceholderText Then
doc.SelectContentControlsByTag("TermsConditions").Item(1).Range.Cut
Else
End If
I'm not sure what you mean by "it is converted to Rich Text"...
The accepted way to insert a BuildingBlock is to use the BuildingBlock.Insert method to which you pass the target Range object. For example (based on your code sample):
Dim doc as Word.Document
Dim rngTarget as Word.Range
Set doc = ActiveDocument
Set rngTarget = doc.SelectContentControlsByTag("TermsConditions").Item(1).Range
doc.AttachedTemplate.BuildingBlockTypes.Item(wdTypeTables).Categories.Item("Terms and and Conditions Translation").BuildingBlocks.Item("Terms and Conditions Eng").Insert rngTarget, true
Take a look at the example in the VBA Language Reference...
Also, you should not use ThisDocument in your code, use ActiveDocument. (Even better, declare a Document object, assign ActiveDocument to it, then use that.)