Word VBA: how to select found text rather than where the cursor is positioned - vba

This is probably simple but I can't get it to work.
I need to search through my document, find words that contain the string 'alog' and add 'ue'. For example, 'catalogs' --> 'catalogues'.
The above works fine but I can't get the next bit to work: if a found string already has 'ue' after the 'log' I don't want to add another 'ue'.
The subroutine accessed from the macro is below. I've tried adding the following lines into the 'while execute' part, but 'selection' always turns out to be the word where the cursor happens to be.
With Selection
.Expand unit:=wdWord
End With
How do I i) select the content of the found range and ii) expand that new selection by two characters to see if those two characters are 'ue' ?
Many thanks.
Sub do_replace2(old_text As String, new_text As String, Count_changes As Integer)
' Replaces 'log' with 'logue'
' Ignores paragraphs in styles beginning with 'Question'
Dim rg As Range
Set rg = ActiveDocument.Range
With rg.Find
.Text = old_text
While .Execute
If Left(rg.Paragraphs(1).Style, 8) <> "Question" Then
rg.Text = new_text
With ActiveDocument.Comments.Add(rg, "Changed from '" & old_text & "'")
.Initial = "-logs"
.Author = "-logs"
End With
Count_changes = Count_changes + 1
End If
rg.Collapse wdCollapseEnd
Wend
End With
End Sub

I'm not quite sure I follow the first part of your question "How do I select the content of the found range". The rg variable already contains the search result. If you want to select it, just use rg.Select. This might be useful in debugging (so you can see where the Range is when you're stepping through the code), but there isn't really any other reason to use the Selection object in the code from your question. You can just use the Range object instead.
As to part 2 of your question "How do I ... expand that new selection by two characters", all you need to do is add 2 to the .End property of the Range. Since you're only using this for a test (and because the .Find method can be dodgy), test this on a copy of rg:
With rg.Find
.Text = old_text
While .Execute
If Left(rg.Paragraphs(1).Style, 8) <> "Question" Then
Dim test As Range
Set test = rg.Duplicate 'copy the found Range.
test.Collapse wdCollapseEnd 'move to the end of it.
test.End = test.End + 2 'expand to the next 2 characters.
If test.Text <> "ue" Then 'see if it's "ue".
rg.Text = new_text
With ActiveDocument.Comments.Add(rg, "Changed from '" & old_text & "'")
.Initial = "-logs"
.Author = "-logs"
End With
Count_changes = Count_changes + 1
End If
End If
rg.Collapse wdCollapseEnd
Wend
End With

Related

What does a hyperlink range.start and range.end refer to?

I'm trying to manipulate some text from a MS Word document that includes hyperlinks. However, I'm tripping up at understanding exactly what Range.Start and Range.End are returning.
I banged a few random words into an empty document, and added some hyperlinks. Then wrote the following macro...
Sub ExtractHyperlinks()
Dim rHyperlink As Range
Dim rEverything As Range
Dim wdHyperlink As Hyperlink
For Each wdHyperlink In ActiveDocument.Hyperlinks
Set rHyperlink = wdHyperlink.Range
Set rEverything = ActiveDocument.Range
rEverything.TextRetrievalMode.IncludeFieldCodes = True
Debug.Print "#" & Mid(rEverything.Text, rHyperlink.Start, rHyperlink.End - rHyperlink.Start) & "#" & vbCrLf
Next
End Sub
However, the output between the #s does not quite match up with the hyperlinks, and is more than a character or two out. So if the .Start and .End do not return char positions, what do they return?
This is a bit of a simplification but it's because rEverything counts everything before the hyperlink, then all the characters in the hyperlink field code (including 1 character for each of the opening and closing field code braces), then all the characters in the hyperlink field result, then all the characters after the field.
However, the character count in the range (e.g. rEverything.Characters.Count or len(rEverything)) only includes the field result if TextRetrievalMode.IncludeFieldCodes is set to False and only includes the field code if TextRetrievalMode.IncludeFieldCodes is set to True.
So the character count is always smaller than the range.End-range.Start.
In this case if you change your Debug expression to something like
Debug.Print "#" & Mid(rEverything.Text, rHyperlink.Start, rHyperlink.End - rHyperlink.Start - (rEverything.End - rEverything.Start - 1 - Len(rEverything))) & "#" & vbCrLf
you may see results more along the lines you expect.
Another way to visualise what is going on is as follows:
Create a very short document with a piece of text followed by a short hyperlink field with short result, followed by a piece of text. Put the following code in a module:
Sub Select1()
Dim i as long
With ActiveDocument
For i = .Range.Start to .Range.End
.Range(i,i).Select
Next
End With
End Sub
Insert a breakpoint on the "Next" line.
Then run the code once with the field codes displayed and once with the field results displayed. You should see the progress of the selection "pause" either at the beginning or the end of the field, as the Select keeps "selecting" something that you cannot actually see.
Range.Start returns the character position from the beginning of the document to the start of the range; Range.End to the end of the range.
BUT everything visible as characters are not the only things that get counted, and therein lies the problem.
Examples of "hidden" things that are counted, but not visible:
"control characters" associated with content controls
"control characters" associated with fields (which also means hyperlinks), which can be seen if field result is toggled to field code display using Alt+F9
table structures (ANSI 07 and ANSI 13)
text with the font formatting "hidden"
For this reason, using Range.Start and Range.End to get a "real" position in the document is neither reliable nor recommended. The properties are useful, for example, to set the position of one range relative to the position of another.
You can get a somewhat more accurate result using the Range.TextRetrievalMode boolean properties IncludeHiddenText and IncludeFieldCodes. But these don't affect the structural elements involved with content controls and tables.
Thank you both so much for pointing out this approach was doomed but that I could still use .Start/.End for relative positions. What I was ultimately trying to do was turn a passed paragraph into HTML, with the hyperlinks.
I'll post what worked here in case anyone else has a use for it.
Function ExtractHyperlinks(rParagraph As Range) As String
Dim rHyperlink As Range
Dim wdHyperlink As Hyperlink
Dim iCaretHold As Integer, iCaretMove As Integer, rCaret As Range
Dim s As String
iCaretHold = 1
iCaretMove = 1
For Each wdHyperlink In rParagraph.Hyperlinks
Set rHyperlink = wdHyperlink.Range
Do
Set rCaret = ActiveDocument.Range(rParagraph.Characters(iCaretMove).Start, rParagraph.Characters(iCaretMove).End)
If RangeContains(rHyperlink, rCaret) Then
s = s & Mid(rParagraph.Text, iCaretHold, iCaretMove - iCaretHold) & "" & IIf(wdHyperlink.TextToDisplay <> "", wdHyperlink.TextToDisplay, wdHyperlink.Address) & ""
iCaretHold = iCaretMove + Len(wdHyperlink.TextToDisplay)
iCaretMove = iCaretHold
Exit Do
Else
iCaretMove = iCaretMove + 1
End If
Loop Until iCaretMove > Len(rParagraph.Text)
Next
If iCaretMove < Len(rParagraph.Text) Then
s = s & Mid(rParagraph.Text, iCaretMove)
End If
ExtractHyperlinks = "<p>" & s & "</p>"
End Function
Function RangeContains(rParent As Range, rChild As Range) As Boolean
If rChild.Start >= rParent.Start And rChild.End <= rParent.End Then
RangeContains = True
Else
RangeContains = False
End If
End Function

Microsoft Word macro to alter heading styles

I am attempting to create a macro in Word that alters the style of a set of ~150 unique headings. All styles must be identical. My current code works and changes the formatting correctly, but only one heading at a time.
Simply put, it's ugly.
I'm looking for something I can reuse, and possibly apply to more projects in the future.
Maybe using the loop command? I don't know, I'm still somewhat new using VBA.
Sub QOS_Headings()
Dim objDoc As Document
Dim head1 As Style, head2 As Style, head3 As Style, head4 As Style
Set objDoc = ActiveDocument
Set head1 = ActiveDocument.Styles("Heading 1")
Set head2 = ActiveDocument.Styles("Heading 2")
With objDoc.Content.Find
.ClearFormatting
.Text = "Section A.^p"
With .Replacement
.ClearFormatting
.Style = head1
End With
.Execute Wrap:=wdFindContinue, Format:=True, Replace:=wdReplaceOne
End With
End With
End Sub
If there is no way in which you can identify the heads you want automatically you may have to write everything once. Create a separate function for this purpose. It might look like this:-
Private Function SearchCriteria() As String()
Dim Fun(6) As String ' Fun = Designated Function return value
' The number of elements in the Dim statement must be equal to
' the number of elements actually declared:
' observe that the actual number of elements is one greater
' than the index because the latter starts at 0
Fun(0) = "Text 1"
Fun(1) = "Text 2"
Fun(2) = "Text 3"
Fun(3) = "Text 4"
Fun(4) = "Text 5"
Fun(5) = "Text 6"
Fun(6) = "Text 7"
SearchCriteria = Fun
End Function
You can add as many elements as you wish. In theory it is enough if they are unique within the document. I shall add some practical concerns below. Use the code below to test the above function.
Private Sub TestSearchCriteria()
Dim Crits() As String
Dim i As Long
Crits = SearchCriteria
For i = 0 To UBound(Crits)
' prints to the Immediate Window:
' select from View tab or press Ctl+G
Debug.Print Crits(i)
Next i
End Sub
Now you are ready to try to actually work on your document. Here is the code. It will not effect any changes. It's just the infrastructure for testing and getting ready.
Sub ChangeTextFormat()
Dim Crits() As String
Dim Rng As Range
Dim Fnd As Boolean
Dim i As Long
Crits = SearchCriteria
For i = 0 To UBound(Crits)
' find the Text in the document
Set Rng = ActiveDocument.Content
With Rng.Find
.ClearFormatting
.Execute FindText:=Crits(i), Forward:=True, _
Format:=False, Wrap:=wdFindStop
Fnd = .Found
End With
If Fnd = True Then
With Rng
Debug.Print .Text
' .MoveStart wdWord, -2
' With .Font
' .Italic = True
' .Bold = True
' End With
End With
Else
Debug.Print "Didn't find " & Crits(i)
End If
Next i
End Sub
The first half of the procedure will find each of the search criteria in your document using the same kind of loop as you already know from the test procedure. But now the text is fed to the Find method which assigns the found text to the Rng range. If the item is found you now have a handle on it by the name of Rng.
The second half of the sub deals with the outcome of the search. If the text was found the found text (that is Rng.Text) is printed to the Immediate window, otherwise the original text Crits(i) with "didn't find".
If the text was found you want to assign a style to it. But before you can do so you should deal with the difference between the text you found and the text you want to format. This difference could be physical, like you didn't write the entire length of the text in the criteria, or technical, like excluding paragraph marks. In my above sub there is just random code (extending the Rng by two preceding words and formatting everything as bold italics). Consider this code a placeholder.
For your purposes code like this might do the job, perhaps. .Paragraphs(1).Style = Head1 Actually, that is rather a different question, and I urge you not to rush for this result too fast. The part you now have needs thorough testing first.

Using word wildcards to find unaccepted changes

I have some word documents with unaccepted, tracked changes. I want to accept them but still have them shown in red in my documents. I think a good way to do this would be doing a wildcard search for unaccepted changes and replacing them with the same text in red, however I dont know if this is possible.
I am also happy with other ways of achieving my goal, without wildcards.
Applying formatting to revisions cannot be done using Word's standard find & replace operation. However, you can write a macro that enumerates all revisions and then applies formatting to each of them.
There is a bloc post by Chris Rae who provides a macro that converts revisions to standard formatting:
Enumerating edits on large documents (AKA converting tracked changes to conventional formatting)
The macro may not yet do exactly what you need, but it should get you started.
For reference, here is a copy of the macro:
Sub EnumerateChanges()
Dim rAll As Revision
Dim dReport As Document
Dim dBigDoc As Document
Set dBigDoc = ActiveDocument
If dBigDoc.Revisions.Count = 0 Then
MsgBox "There are no revisions in the active document.", vbCritical
ElseIf MsgBox(“This will enumerate the changes in '" + dBigDoc.Name + "' in a new document and close the original WITHOUT saving changes. Continue?", vbYesNo) <> vbNo Then
Set dReport = Documents.Add
dBigDoc.Activate ' really just so we can show progress by selecting the revisions
dBigDoc.TrackRevisions = False ' Leaving this on results in a disaster
For Each rAll In dBigDoc.Revisions
' Now find the nearest section heading downwards
Dim rFindFirst As Range, rFindLast As Range
Set rFindLast = rAll.Range.Paragraphs(1).Range
While Not IsNumberedPara(rFindLast.Next(wdParagraph))
Set rFindLast = rFindLast.Next(wdParagraph)
Wend
' Now head back up to the next numbered section header
Set rFindFirst = rFindLast
Do
Set rFindFirst = rFindFirst.Previous(wdParagraph)
Loop Until IsNumberedPara(rFindFirst) Or (rFindFirst.Previous(wdParagraph) Is Nothing)
ConvertNumberedToText rFindFirst
Dim rChangedSection As Range
Set rChangedSection = dBigDoc.Range(rFindFirst.Start, rFindLast.End)
' Properly tag all the revisions in this whole section
Dim rOnesInThisSection As Revision
For Each rOnesInThisSection In rChangedSection.Revisions
rOnesInThisSection.Range.Select ' just for visual update
DoEvents ' update the screen so we can see how far we are through
If rOnesInThisSection.Type = wdRevisionDelete Then
rOnesInThisSection.Reject
With Selection.Range
.Font.ColorIndex = wdRed
.Font.StrikeThrough = True
End With
dBigDoc.Comments.Add Selection.Range, “deleted”
Else
If rOnesInThisSection.Type = wdRevisionInsert Then
rOnesInThisSection.Accept
With Selection.Range
.Font.ColorIndex = wdBlue
End With
dBigDoc.Comments.Add Selection.Range, “inserted”
End If
End If
Next
' Now copy the whole thing into our new document
rChangedSection.Copy
Dim rOut As Range
Set rOut = dReport.Range
rOut.EndOf wdStory, False
rOut.Paste
Next rAll
' There should end up being no numbered paragraphs at all in the
' new doc (they were converted to text), so delete them
Dim pFinal As Paragraph
For Each pFinal In dReport.Paragraphs
If IsNumberedPara(pFinal.Range) Then
pFinal.Range.ListFormat.RemoveNumbers
End If
Next
dBigDoc.Close False
End If
End Sub
Sub ConvertNumberedToText(rOf As Range)
If InStr(rOf.ListFormat.ListString, “.”) > 0 Then
rOf.InsertBefore "Changes to section " + rOf.ListFormat.ListString + " "
End If
End Sub
Function IsNumberedPara(rOf As Range) As Boolean
If rOf Is Nothing Then ‘ if the document doesn’t have numbered sections, this will cause changes to be enumerated in the whole thing
IsNumberedPara = True
ElseIf rOf.ListFormat.ListString <> "" Then
If Asc(rOf.ListFormat.ListString) <> 63 Then
IsNumberedPara = True
End If
End If
End Function

Loop through pages OR page breaks?

I'm basically trying to create a cumulative word count for documents that will put the number of words on each page into its footer and add it to the total words each page. After a lot of looking around, I found that Word doesn't really handle pages the same for everybody and so doesn't have any interface to access the individual pages through.
Now I'm trying to separate each page with page breaks so there's a clear delimiter between pages, but I still can't find how to loop through these. Any clues?
I'm going to post the code I have, but it's only for getting the word count currently. No proper attempts at cycling through page breaks because I don't know how.
Sub getPageWordCount()
Dim iPgNum As Integer
Dim sPgNum As String
Dim ascChar As Integer
Dim rngPage As Range
Dim iBeginPage As Integer
Dim iEndPage As Integer
' Go to start of document and make sure its paginated correctly.
Selection.HomeKey Unit:=wdStory, Extend:=wdMove
ActiveDocument.Repaginate
' Loop through the number of pages in the document.
For iPgNum = 2 To Selection.Information(wdNumberOfPagesInDocument)
sPgNum = CStr(iPgNum)
iBeginPage = Selection.Start
' Go to next page
Selection.GoTo wdGoToPage, wdGoToAbsolute, sPgNum
' and to the last character of the previous page...
Selection.MoveLeft wdCharacter, 1, wdMove
iEndPage = Selection.Start
' Retrieve the character code at insertion point.
Set rngPage = ActiveDocument.Range(iBeginPage, iEndPage)
MsgBox rngPage.ComputeStatistics(wdStatisticWords)
'rngPage.Footers(wdHeaderFooterPrimary).Range.Text = rngPage.ComputeStatistics(wdStatisticWords)
'ActiveDocument.Sections(2).Footers
' Check the character code for hard page break or text.
Next
' ActiveDocument.Sections(2).Footers(wdHeaderFooterPrimary).Range.Text = "bob" 'Testing
End Sub
Finally got it, managed to guess my way through it a bit, taking assorted bits from dark corners of the internet:
Sub getPageWordCount()
'Replace all page breaks with section breaks
Dim myrange1 As Range, myrangedup As Range
Selection.HomeKey wdStory
Selection.Find.ClearFormatting
With Selection.Find
Do While .Execute(findText:="^m", Forward:=True, _
MatchWildcards:=False, Wrap:=wdFindStop) = True
Set myrange = Selection.Range
Set myrangedup = Selection.Range.Duplicate
myrange.Collapse wdCollapseEnd
myrange.InsertBreak wdSectionBreakNextPage
myrangedup.Delete
Loop
End With
'Unlink all footers and insert word count for each section
Dim sectionCount, sectionNumber, i, sectionWordCount, cumulativeWordCount As Integer
sectionCount = ActiveDocument.Sections.Count
For sectionNumber = 1 To sectionCount
With ActiveDocument.Sections(sectionNumber)
sectionWordCount = .Range.ComputeStatistics(wdStatisticWords)
cumulativeWordCount = cumulativeWordCount + sectionWordCount
With .Footers.Item(1)
.LinkToPrevious = False
.Range.Text = "This page's word count: " + CStr(sectionWordCount) + " | Cumulative word count: " + CStr(cumulativeWordCount)
.Range.ParagraphFormat.Alignment = wdAlignParagraphCenter
End With
End With
Next
End Sub
And now I've just discovered that if I want to port this macro to an add-in for ease of use for non-techy users I have to write it in VB 2010 in Visual Studio where the API is different. Good luck me!
It sounds as if you have what you need, but I was working on an alternative that I may as well post because it does not require you to add page breaks or section breaks. But you would have to add the same nested field in each footer that appears in the document (I haven't done that part here, but it's not completely trivial because there may be multiple sections and multiple footers per section).
The field code you need to add (in addition to your 'This page's word count: ' text) is
{ DOCVARIABLE "s{ SECTION }p{ PAGE \*arabic }" }
As written, the method may break in some circumstances, e.g. if there are continuous section breaks. I haven't checked.
Sub createWordCounts()
Dim i As Integer
Dim rng As Word.Range
With ActiveDocument
For i = 1 To .Range.Information(wdActiveEndPageNumber)
Set rng = .GoTo(wdGoToPage, wdGoToAbsolute, i).Bookmarks("\page").Range
.Variables("s" & CStr(rng.Information(wdActiveEndSectionNumber)) & "p" & CStr(rng.Information(wdActiveEndAdjustedPageNumber))).Value = rng.ComputeStatistics(wdStatisticWords)
Set rng = Nothing
Next
End With
End Sub

How do you remove hyperlinks from a Microsoft Word document?

I'm writing a VB Macro to do some processing of documents for my work.
The lines of text are searched and the bracketed text is put in a list(box).
The problem comes when I want to remove all hyperlinks in the document and then generate new ones (not necessarily in the location of the original hyperlinks)
So the problem is How do I remove the existing hyperlinks?
My current issue is that every time a link gets added, the hyperlinks count goes up one, but when you delete it, the count does NOT reduce. (as a result I now have a document with 32 links - all empty except for 3 I put in myself - they do not show up in the document)
At the end of the code are my attempts at removing the hyperlinks.
Private Sub FindLinksV3_Click()
ListOfLinks.Clear
ListOfLinks.AddItem Now
ListOfLinks.AddItem ("Test String 1")
ListOfLinks.AddItem ActiveDocument.FullName
SentenceCount = ActiveDocument.Sentences.Count
ListOfLinks.AddItem ("Sentence Count:" & SentenceCount)
counter = 0
For Each myobject In ActiveDocument.Sentences ' Iterate through each element.
ListOfLinks.AddItem myobject
counter = counter + 1
BracketStart = (InStr(1, myobject, "("))
If BracketStart > 0 Then
BracketStop = (InStr(1, myobject, ")"))
If BracketStop > 0 Then
ListOfLinks.AddItem Mid$(myobject, BracketStart + 1, BracketStop - BracketStart - 1)
ActiveDocument.Sentences(counter).Select
ActiveDocument.Hyperlinks.Add Anchor:=Selection.Range, Address:= _
"http://testnolink/" & counter, ScreenTip:="" 'TextToDisplay:=""
End If
End If
Next
'ActiveDocument.Sentences(1).Select
'
'Selection.Range.Hyperlinks(1).Delete
ActiveDocument.Hyperlinks.Item(1).Delete
Debug.Print ActiveDocument.Hyperlinks.Count
End Sub
This is an old post, so am adding this VBA code in case it is useful to someone.
Hyperlinks (Collections) need to be deleted in reverse order:
Sub RemoveHyperlinksInDoc()
' You need to delete collection members starting from the end going backwards
With ActiveDocument
For i = .Hyperlinks.Count To 1 Step -1
.Hyperlinks(i).Delete
Next
End With
End Sub
Sub RemoveHyperlinksInRange()
' You need to delete collection members starting from the end going backwards
With Selection.Range
For i = .Hyperlinks.Count To 1 Step -1
.Hyperlinks(i).Delete
Next
End With
End Sub
The line removing the hyperlink is commented out. The following line will remove the first hyperlink within the selected range:
Selection.Range.Hyperlinks(1).Delete
This will also decrement Selection.Range.Hyperlinks.Count by 1.
To see how the count of links is changing you can run the following method on a document:
Sub AddAndRemoveHyperlink()
Dim oRange As Range
Set oRange = ActiveDocument.Range
oRange.Collapse wdCollapseStart
oRange.MoveEnd wdCharacter
Debug.Print ActiveDocument.Range.Hyperlinks.Count
ActiveDocument.Hyperlinks.Add oRange, "http://www.example.com"
Debug.Print ActiveDocument.Range.Hyperlinks.Count
ActiveDocument.Hyperlinks.Item(1).Delete
Debug.Print ActiveDocument.Range.Hyperlinks.Count
End Sub