TLDR: having successfully used a .FIND object to locate some text in a Word document, how can I find out what page this text was found on?
Longer explanation: I have a Word document of shipping labels, one per page. I want to search this file for a specific order number and make a note of the filename and page number. This is so that when my app (MS Access using Word Object Library) later dispatches that order to the customer it can automatically open the correct label file and print the label for that order.
Here is my (simplified) code below. The find is working (Find.Found = True) but I am struggling to work out how to detect the page number. The code shown always returns the last page of the document. If I run the code with the Word document visible I can see that although the .find.execute is working the document is not moving to the correct page.
Dim Find As Word.Find
Dim LabelPageNum as Integer
LabelPageNum = 0
Set Find = Word.ActiveDocument.Content.Find
Find.ClearFormatting
Find.Text = "ABC-123456"
Find.Forward = True
Find.Wrap = wdFindContinue
Find.Format = False
Find.MatchWildcards = True
Find.IgnoreSpace = True
Find.IgnorePunct = True
Find.Execute
If Find.Found Then
LabelPageNum = Word.ActiveDocument.Content.Information(wdActiveEndPageNumber)
End if
Find.Parent.Information(wdActiveEndPageNumber) will give you the result.
Find.Parent returns the range that has been found - and on that you apply Information.
BTW: It's good coding practice to not use code words as variable names
If Find.Found Then LabelPageNum = .Information(wdActiveEndPageNumber)
Related
This is very easy to replicate....Please follow my steps with MS Word.
In a Word file, create a fairly large table (of about five columns and twenty rows). Populate all cells with some junk text.
Or, pick a "good sized" table in a file that you already have.
Select (or "Highlight") a few rows of the table, such as three rows.
Go to the Font section of the Word Ribbon. Notice that the Hidden
property of the selection is showing as "not checked" which means
visible. This is what you expect to see (for now).
Optionally, also go to the Immediate Window of the VB Editor and
type "? Selection.Range.Font.Hidden". Word will respond "0", meaning
the .Hidden property is false. This is another way of verifying what
you saw in Step 2.
Now select an additional few rows of the table. Go to Step 2 and repeat.
By the time your Selection gets up to about 12 rows (your results may vary somewhat), the Hidden checkbox shows a solid block to indicate "mixed hidden-ness", and "? Selection.Range.Font.Hidden" shows 9999999 to indicate 'undefined" state.
WHY does the .Hidden property become unreliable/unreadable when the selection is in a table and the selection is relatively many rows? This does not happen when the Selection in a non-table. This is messing up a program of mine that is otherwise very good.
Thanks to all who puzzled over this bug in MS Word. My workaround is as follows. I make the assumption that the hidden-ness (or other property of the selection like .Font or .Underline) of the the selection is "uniform" ... that I can try to read the property of the first 10 characters rather than the entire selection, and still get the right answer. If you do not know that your entire selection has the same properties as the first 10 characters of your selection, this solution won't help you.
Function DigDeeperInto(fullSelection As Selection) As String
' This returns the truth of the hidden-ness of the *front end* of a Selection.
' This is used because Word sometimes mis-reads the hidden-ness of a whole Selection.
Dim shortRange As Range ' will hold the just the front end of the full Selection
Dim theTruth As String
Set shortRange = fullSelection.Range
shortRange.SetRange Start:=shortRange.Start + 1, End:=shortRange.Start + 10
If shortRange.Font.Hidden = wdUndefined Then theTruth = "UnDef" ' this Function failed
If shortRange.Font.Hidden = 0 Then theTruth = "Visible"
If shortRange.Font.Hidden = -1 Then theTruth = "Hidden"
DigDeeperInto = theTruth
End Function
After selecting a Selection, and assuming that Selection.Font.Hidden will not be reported correctly, I call the DigDeeperInto(Selection) function. It looks at only the first 10 characters and returns a string "Visible", "Hidden", or "UnDef". If it still returns "UnDef" (which does not happen for me, luckily), that would indicate DigDeeperInto() failed to solve the problem.
For what it's worth this is not specific to tables; the exact same thing happens in a document that has 51 blank (but otherwise identical) paragraphs. If you select 50 paragraphs you can get real results for:
?selection.Range.Paragraphs.Count ' just to confirm the selection
?selection.Font.name
?selection.Range.Font.Hidden
BUT as soon as you select the 51st paragraph things lose definition, so to speak.
I imagine that looping over your table row by row (as suggested by #jsotola) would be a good workaround.
To answer the question as to WHY this happens, well the best anyone outside of MS can do (I think) is speculate. If I were to speculate I would suggest that there is some sort of internal limit or limitation when it comes to processing or storing detailed information for so many paragraphs. Is it a bug, I think so (but that is just IMHO).
Function to replace the buggy built-in function
Example of a function that will return True, False or wdUndefined (9999999) depending on whether font is Hidden, Visible or a mixture of Hidden & Visible (i.e. undefined), respectively.
Note that it is entirely possible for wdUndefined to be a valid result as an otherwise visible paragraph may have a single character hidden by font and therefore that paragraph's font would neither be entirely visible, not hidden - so it is 'undefined' - hence wdUndefined is very valid.
Example usage and output:
?selection.Paragraphs.Count
2016
?fontIsHidden '(automatically operates on the active selection)
False
The function:
' Put this code into a STANDARD module
Function fontIsHidden() As Variant ' Variant to return True, False, wdUndefined
Dim paraSelected As Paragraph
Dim NotFirstPara As Boolean
Dim result As String
For Each paraSelected In Selection.Range.Paragraphs
' This could be done with Table.Rows instead of Paragraps
' (but would error if there was no table)
If NotFirstPara Then
If paraSelected.Previous.Range.Font.Hidden <> paraSelected.Range.Font.Hidden Then
result = wdUndefined ' this para is different to the previous paras
Exit For
End If
Else
' Setup for the first paragraph
Select Case paraSelected.Range.Font.Hidden
Case False ' 0
result = False
Case True ' -1
result = True
Case wdUndefined
result = "Undefined" ' some text in the para is visible and some is hidden
End Select
NotFirstPara = True
End If
Next paraSelected
fontIsHidden = result
End Function
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..
I am looking to adapt the code presented by Santosh so that I can return the top ten links, not just the top one result, of a particular Google search. I need to upload > 1000 search queries and map the results against expected results but I care about more than just the #1 result, I'm looking to see if it returns within the top ten. I looked into the html and VBA and I can't figure it out.
Using VBA in Excel to Google Search in IE and return the hyperlink of the first result
Without testing, it looks like this line here is pulling one element in a collection:
Set link = objH3.getelementsbytagname("a")(0)
So, you're setting the "Link" object to the first object (object 0) in the objH3 collection that has the "a" tag.
What you want to do is loop through that collection. Ex:
Set links = objH3.getelementsbytagname("a")
For i = 0 To 9
set link = links(i)
'do stuff
next
Edit:
The loop should be on a higher level of objects:
For i = 0 To 5
Set objH3 = objResultDiv.getelementsbytagname("H3")(i)
Set link = objH3.getelementsbytagname("a")(0)
' Do Stuff
Next I
Thank you, Richard, for the update.
I have the unfortunate task of being forced to design a Word-based electronic production card for the unit at my company, even though I've never worked with VBA. I would much rather have done this in Excel since I wouldn't have to wrestle with content control and hard-to-find locations in various tables over the pages, but the company's documentation-system forces this particular one to be in Word.
My issue is that for proper form of the production card I need to use tables, and I need the production card to be dynamic to limit its size to what operations that are relevant for a specific order. My chosen solution is to create a full form, and to use a user form/prompt where they can choose which parts to use and which parts to ommit, and the ommitted ones will then be deleted. Part of reason for the solution is because that is how their previous (and Excel-based) production card works, so it would make it more familiar for the end user.
Because MS Word is finicky I need to use content control within these tables to not have the end user accidentally destroy half of it, but after a full workday I still cannot figure out how find and shut off the content control of the cells in tables that I want to delete. I do have the content controls tagged since that seems like the only reasonable way to find them.
This is my current code for the subprocedure, but for some reason I cannot get the ID through the ccID line, even though I have verified that the string supplied as argument is correct.
Private Sub DeleteCCByTag(ccTag As String)
Dim cc As ContentControl
Dim ccID As String
ccID = ThisDocument.SelectContentControlsByTag(ccTag).Item(1).ID
'MsgBox ccID 'Debug prompt
Set cc = ThisDocument.ContentControls(ccID)
cc.LockContentControl = False
cc.LockContents = False
cc.Delete (False)
End Sub
First of all- your code is working find for me but...
ContentControls tag is case-sensitive which could be a problem in your situation
You could solve your problem without searching for ID value in this way:
Private Sub DeleteCCByTag_Alternative(ccTag As String)
Dim cc As ContentControl
Set cc = ThisDocument.SelectContentControlsByTag(ccTag).Item(1)
With cc
.LockContentControl = False
.LockContents = False
.Range.Delete 'to delete CC content
.Delete (False)
End With
End Sub
CC.Delete in your code deletes only ContentControl objects itself but not its content. To delete content you need to add additional line which I did in my code above.
I would add this as a comment to KazJaw's answer but I don't have the rep.
According to Microsoft's documentation, if you pass True to the Delete method it removes both the content control and its contents.
So: just get the Item as KazJaw showed, without jumping through the hoop of getting its ID:
Set cc = ThisDocument.SelectContentControlsByTag(ccTag).Item(1)
then call .Delete(True) on it.
I am making lots of changes to a Word document using automation, and then running a VBA macro which - among other things - checks that the document is no more than a certain number of pages.
I'm using ActiveDocument.Information(wdNumberOfPagesInDocument) to get the number of pages, but this method is returning an incorrect result. I think this is because Word has not yet updated the pagination of the document to reflect the changes that I've made.
ActiveDocument.ComputeStatistics(wdStatisticPages) also suffers from the same issue.
I've tried sticking in a call to ActiveDocument.Repaginate, but that makes no difference.
I did have some luck with adding a paragraph to the end of the document and then deleting it again - but that hack seems to no longer work (I've recently moved from Word 2003 to Word 2010).
Is there any way I can force Word to actually repaginate, and/or wait until the repagination is complete?
I just spent a good 2 hours trying to solve this, and I have yet to see this answer on any forum so I thought I would share it.
https://msdn.microsoft.com/en-us/vba/word-vba/articles/pages-object-word?f=255&MSPPError=-2147217396
That gave me my solution combined with combing through the articles to find that most of the solutions people reference are not supported in the newest versions of Word. I don't know what version it changed in, but my assumption is that 2013 and newer can use this code to count pages:
ActiveDocument.ActiveWindow.Panes(1).Pages.Count.
I believe the way this works is ActiveDocument selects the file, ActiveWindow confirms that the file to be used is in the current window (in case the file is open in multiple windows from the view tab), Panes determines that if there is multiple windows/split panes/any other nonsense you want the "first" one to be evaluated, pages.count designates the pages object to be evaluated by counting the number of items in the collection.
Anyone more knowledgeable feel free to correct me, but this is the first method that gave me the correct page count on any document I tried!
Also I apologize but I cant figure out how to format that line into a code block. If the mods want to edit my comment to do that be my guest.
Try (maybe after ActiveDocument.Repaginate)
ActiveDocument.BuiltinDocumentProperties(wdPropertyPages)
It is causing my Word 2010 to spend half-second with "Counting words" status in status bar, while ActiveDocument.ComputeStatistics(wdStatisticPages) returns the result immediately.
Source: https://support.microsoft.com/en-us/kb/185509
After you've made all your changes, you can use OnTime to force a slight delay before reading the page statistics.
Application.OnTime When:=Now + TimeValue("00:00:02"), _
Name:="UpdateStats"
I would also update all the fields before this OnTime statement:
ActiveDocument.Range.Fields.Update
I found a possible workaround below, if not a real answer to the topic question.
Yesterday, the first ComputeStatistics line below was returning the correct total of 31 pages, but today it returns only 1.
The solution is to get rid of the Content object and the correct number of pages is returned.
Dim docMultiple As Document
Set docMultiple = ActiveDocument
lPageCount = docMultiple.Content.ComputeStatistics(wdStatisticPages) ' Returns 1
lPageCount = docMultiple.ComputeStatistics(wdStatisticPages) ' Returns correct count, 31
ActiveDocument.Range.Information(wdNumberOfPagesInDocument)
This works every time for me. It returns total physical pages in the word.
I used this from within Excel
it worked reliably on about 20 documents
none were longer than 20 pages but some were quite complex
with images and page breaks etc.
Sub GetlastPageFromInsideExcel()
Set wD = CreateObject("Word.Application")
Set myDoc = wD.Documents.Open("C:\Temp\mydocument.docx")
myDoc.Characters.Last.Select ' move to end of document
wD.Selection.Collapse ' collapse selection at end
lastPage = wD.Selection.Information(wdActiveEndPageNumber)
mydoc.close
wd.quit
Set wD = Nothing
End Sub
One problem I had in getting "ComputeStatistics" to return a correct page count was that I often work in "Web Layout" view in Word. Whenever you start Word it reverts to the last view mode used. If Word was left in "Web Layout" mode "ComputeStatistics" returned a page count of "1" page for all files processed by the script. Once I specifically set "Print Layout" view I got the correct page counts.
For example:
$MSWord.activewindow.view.type = 3 # 3 is 'wdPrintView', 6 is 'wdWebView'
$Pages = $mydoc.ComputeStatistics(2) # 2 is 'wdStatisticPages'
You can use Pages-Object and its properties such as Count. It works perfect;)
Dim objPages As Pages
Set objPage = ActiveDocument.ActiveWindow.Panes(1).Pages
QuantityOfPages = ActiveDocument.ActiveWindow.Panes(1).Pages.Count
Dim wordapp As Object
Set wordapp = CreateObject("Word.Application")
Dim doc As Object
Set doc = wordapp.Documents.Open(oFile.Path)
Dim pagesCount As Integer
pagesCount = doc.Content.Information(4) 'wdNumberOfPagesInDocument
doc.Close False
Set doc = Nothing