Find range by format and then change that format (not using the replace option) - vba

Newbie Jan here with his first question
I am trying to understand the behavior of my vba code. The code is searching for words/sentences with a specific format. When found I would like to adjust these words/sentences by modifying the range that is returned. If I adjust the range as this
that does not produce any problem (loop continues). If I delete the returned range the loop also continues.
Dim myFind As Find
Dim myRange As Range
Set myRange = Application.ActiveDocument.Content
Set myFind = myRange.Find
With myFind
.ClearFormatting
.Font.Underline = wdUnderlineDouble
.Font.Italic = True
End With
Do While myFind.Execute = True
myRange.Font.Bold = True
myRange.Font.StrikeThrough = True
Loop
However If I adjust the format that is used in the search (myRange.font.italic = false) then the loop exit after the first found.
Dim myFind As Find
Dim myRange As Range
Set myRange = Application.ActiveDocument.Content
Set myFind = myRange.Find
With myFind
.ClearFormatting
.Font.Underline = wdUnderlineDouble
.Font.Italic = True
End With
Do While myFind.Execute = True
myRange.Font.Italic = False
Loop
Can someone help me understand why this happens? If I delete the range (myRange.delete) the loop continues (which confuses me). It seems I cannot undo the formatting of what I am searching in this way... but deleting the range gives no problem. I tried to find documentation about this but I am unable to find information about my specific problem.
I have worked around this by selecting the range and then executing the next find and then modifying the selection. This works... but I would still like to understand what is happening.
P.S. This is just a part of the code. The code will be used to create some revisions. That is the reason I do not use the find/replacement options
Thanks!
some extra context
I am dealing with big word files and I want to automatically find 'specially formatted text' and modify this text automatically. Eventually I want the formatted text to become a track change/revision (that is also the reason I do not use the find/replace options). But I would first like to understand what is happening in my code. I understand that the .execute is returning a false and that this is the reason of the exit of the loop. I do not understand why .execute is returning a false while there are still other words/sentences in the document that have the format I was searching for

When using Find in VBA it is best to set .Wrap = wdFindStop to avoid getting into a continuous loop.
If you are not going to use Replace it is also good practice to assign the result of Execute to a variable (although there is a Found property it is considered unreliable).
Performing actions on the found range and then continuing the Find requires the use of Execute again in the loop. It is also necessary to collapse the found range so that Find can continue past it.
When the Find criteria are met the range is redefined to the found range. If you change the properties of that range so that they no longer meet the Find criteria Do While myFind.Execute = True evaluates as False, ending the loop.
The following routine works for all the conditions you mention in your question.
Sub FindFormatting()
Dim findRange As Range
Dim findSucess As Boolean
Set findRange = ActiveDocument.Content
With findRange.Find
.ClearFormatting
.Font.Underline = wdUnderlineDouble
.Font.Italic = True
.Wrap = wdFindStop
findSucess = .Execute
Do While .Found
With findRange.Font
.Italic = False
End With
'collapse range to continue
findRange.Collapse wdCollapseEnd
findSucess = .Execute
Loop
End With
End Sub

This probably should be a comment, but I am too new to add a comment.
I can verify that this happens on my system. To clarify, what you are trying to do is change the formatting for your targets to remove them from the search criteria after you have done your double-strikethrough and bold?
Your code is exiting the loop after the first pass to remove the Italics. That means it has finished the find. I can't explain this.
When I substitute the following for the code changing Italics, I get a message box for each instance found.
`MsgBox "Still in Loop"`
I'm still not clear on why you are not using the Replace function.
================== EDIT - ADDED IN RESPONSE TO COMMENT ==================
Try the following. It is going through the document multiple times but seems to work.
`Sub replacer()
' https://stackoverflow.com/questions/63766819/ms-word-vba-find-object-by-format-and-then-change-that-format-not-using-the-rep
Dim myFind As Find
Dim myRange As Range
Set myRange = Application.ActiveDocument.Content
Set myFind = myRange.Find
With myFind
.ClearFormatting
.Font.Underline = wdUnderlineDouble
.Font.Italic = True
End With
Do While myFind.Execute = True
myRange.Font.Bold = True
myRange.Font.StrikeThrough = True
Loop
'
StartOver:
Set myRange = Application.ActiveDocument.Content
Set myFind = myRange.Find
With myFind
.ClearFormatting
.Font.Underline = wdUnderlineDouble
.Font.Italic = True
.Font.StrikeThrough = True
.Font.Bold = True
End With
Do While myFind.Execute = True
' MsgBox "Still in Loop"
myRange.Font.Italic = False
If myFind.Found = True Then GoTo StartOver
Loop
End Sub`
I still have no explanation for why this is happening.

Related

How to print row of found string?

I'd like to find several strings within Word document and for each string found, I like to print (debug.print for example) the whole row content where the string is found, not the paragraph.
How can I do this? Thanks
Sub FindStrings
Dim StringsArr (1 to 3)
StringsArr = Array("string1","string2","string3")
For i=1 to 3
With
Selection.Find
.ClearFormatting
.Text = Strings(i)
Debug.Print CurrentRow 'here I need help
End With
Next
End Sub
The term Row in Word is used only in the context of a table. I assume the term you mean is Line, as in a line of text.
The Word object model has no concept of "line" (or "page") due to the dynamic layout algorithm: anything the user does, even changing the printer, could change where a line or a page breaks over. Since these things are dynamic, there's no object.
The only context where "line" can be used is in connection with a Selection. For example, it's possible to extend a Selection to the start and/or end of a line. Incorporating this into the code in the question it would look something like:
Sub FindStrings()
Dim StringsArr As Variant
Dim bFound As Boolean
Dim rng As Word.Range
Set rng = ActiveDocument.content
StringsArr = Array("string1", "string2", "string3")
For i = LBound(StringsArr) To UBound(StringsArr)
With rng.Find
.ClearFormatting
.Text = StringsArr(i)
.Wrap = wdFindStop
bFound = .Execute
'extend the selection to the start and end of the current line
Do While bFound
rng.Select
Selection.MoveStart wdLine, -1
Selection.MoveEnd wdLine, 1
Debug.Print Selection.Text
rng.Collapse wdCollapseEnd
bFound = .Execute
Loop
End With
Set rng = ActiveDocument.content
Next
End Sub
Notes
Since it's easier to control when having to loop numerous times, a Range object is used as the basic search object, rather than Selection. The found Range is only selected for the purpose of getting the entire line as these "Move" methods for lines only work on a Selection.
Before the loop can continue, the Range (or, if we were working with a selection, the selection) needs to be "collapsed" so that the code does not search and find the same instance of the search term, again. (This is also the reason for Wrap = wdFindStop).

Identify Invalid Characters

I need to loop through each letter of a word document and compare each against a list of valid characters. If the current character is not in the list, its font color should be changed.
I am a newbie to VBA and have written a small looping code but it takes very long to go through even a small word file. Code is below -
Sub LoopThruFile()
Dim doc As Document
Dim CurrChar As String
Application.ScreenUpdating = False
Set doc = ActiveDocument
For i = 1 To doc.Range.Characters.Count
CurrChar = doc.Range.Characters(i)
If InStr("01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-_()/#:&\%", CurrChar) = 0 Then
doc.Range.Characters(i).Font.ColorIndex = wdRed
End If
Next
Application.ScreenUpdating = True
End Sub
Is there a better and faster code to do this?
Looping individual characters is slow. One thing that could speed performance in your example is to reduce the number of hierarchy levels working against the Range directly:
Dim doc as Document
Dim docRange as Range
Dim CurrChar as String
Set doc = ActiveDocument
Set docRange = doc.Content
Note that Document.Range is actually a method that expects two parameters; Document.Content automatically returns the entire Range as a property, so would be more correct.
Then, the Character object is actually a Range object. The VBA has to convert your line of code to include the Text property in order to assign CurrChar to a String. Probably doesn't make a lot of difference, but more correct and probably a little faster:
CurrChar = docRange.Characters(i).Text
Sometimes a loop can be faster if you run from the end of the document to the beginning:
For i = docRange.Characters.Count to 1 Step -1
CurrChar = docRange.Characters(i).Text
If InStr("01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-_()/#:&\%", CurrChar) = 0 Then
docRange.Characters(i).Font.ColorIndex = wdRed
End If
Next
You could also try using For Each, which might be the fastest in this scenario since you save multiple calls of doc.Range.Characters(i) each of which consumes resources.
Dim CurrChar as Range
For Each CurrChar in docRange.Characters
If InStr("01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-_()/#:&\%", CurrChar.Text) = 0 Then
CurrChar.Font.ColorIndex = wdRed
End If
Next
The following worked for me. Try this:
With ActiveDocument.Content.Find
.ClearFormatting
.Replacement.ClearFormatting
.text = "[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0-9\,\.\-_\(\)\/\\\#\:\&\%]"
.Replacement.text = ""
.Replacement.Font.Color = wdColorRed
.MatchWildcards = True
.Execute Replace:=wdReplaceAll
End With

Word VBA: finding a set of words and inserting predefined comments

I need to automate the insertion of comments into a word document: searching for a predefined set of words (sometimes word strings, and all non case-sensitive) each to which I add a predefined comment.
There are two word sets, with two goals:
Wordset 1: identical comment for each located word
Wordset 2: individual comments (I suggest new text based on the word identified)
I have been semi-automating this with a code that IDs all identified words and highlights them, helping me through the process (but I still need to enter all the comments manually - and I've also been able to enter comments - but only on one word at a time.) As my VBA skills are limited, my attempts to compile a robust macro from bits of other code with similar purposes has unfortunately led me nowhere.
Below are the bits of code I've been using.
Sub HighlightWordList()
Dim range As range
Dim i As Long
Dim TargetList
TargetList = Array("word1", "word2", "word3")
For i = 0 To UBound(TargetList)
Set range = ActiveDocument.range
With range.Find
.Text = TargetList(i)
.Format = True
.MatchCase = True
.MatchWholeWord = False
.MatchWildcards = False
.MatchSoundsLike = False
.MatchAllWordForms = False
Do While .Execute(Forward:=True) = True
range.HighlightColorIndex = wdYellow
Loop
End With
Next
End Sub
The following code has been able to get me to insert bubbles directly
Sub CommentBubble()
'
'
Dim range As range
Set range = ActiveDocument.Content
Do While range.Find.Execute("Word x") = True
ActiveDocument.Comments.Add range, "my comment to enter in the bubble"
Loop
End Sub
I've tried to have the process repeat itself by doing as shown below, but for reasons I'm certain are evident to many of you (and completely unknown to me) - this strategy has failed, working for "word x" but failing to function for all subsequent words:
Sub CommentBubble()
'
'
Dim range As range
Set range = ActiveDocument.Content
Do While range.Find.Execute("Word x") = True
ActiveDocument.Comments.Add range, "my 1st comment to enter in the bubble"
Loop
Do While range.Find.Execute("Word y") = True
ActiveDocument.Comments.Add range, "my 2nd comment to enter in the bubble"
Loop
End Sub
I've mixed and matched bits of these codes to no avail. Any ideas to help me with either wordset?
Thanks for everyone's help!
Best regards
Benoit, you're almost there! All you need to do is redefine the range object after your first loop (because it would have been exhausted at that point). Like so:
Sub CommentBubble()
Dim rng As range
Set rng = ActiveDocument.Content
Do While rng.Find.Execute("Word x") = True
ActiveDocument.Comments.Add rng, "my 1st comment to enter in the bubble"
Loop
Set rng = ActiveDocument.Content ' <---------------Add This.
Do While rng.Find.Execute("Word y") = True
ActiveDocument.Comments.Add rng, "my 2nd comment to enter in the bubble"
Loop
End Sub
That should do the trick for you (it works on my end). If not, let me know.

Word VBA: How to replace only the next instance of a string via Replacement Object

This is a silly question, but can't figure it out.
Straight from the Microsoft Site:
This example finds every instance of the word "Start" in the active document and replaces it with "End." The find operation ignores formatting but matches the case of the text to find ("Start").
Set myRange = ActiveDocument.Range(Start:=0, End:=0)
With myRange.Find
.ClearFormatting
.Text = "Start"
With .Replacement
.ClearFormatting
.Text = "End"
End With
.Execute Replace:=wdReplaceAll, _
Format:=True, MatchCase:=True, _
MatchWholeWord:=True
End With
I need to know how to make it so it only finds the next instance of Start and replace it with End. This will leave all other Ends intact throughout the document.
You should use wdReplaceOne in place of wdReplaceAll.
You should be able to adapt this:
Sub Tester()
Const FIND_WHAT as String = "Start"
Const REPLACE_WITH as String = "End"
Const REPLACE_WHICH As Long = 4 'which instance to replace?
Dim rng As Range, i As Long
i = 0
Set rng = ActiveDocument.Content
With rng.Find
.ClearFormatting
.Text = FIND_WHAT
Do While .Execute(Format:=True, MatchCase:=True, _
MatchWholeWord:=True)
i = i + 1
If i = REPLACE_WHICH Then
'Note - "rng" is now redefined as the found range
' This happens every time Execute returns True
rng.Text = REPLACE_WITH
Exit Do
End If
Loop
End With
End Sub
This discussion has some useful suggestions: Replace only last occurrence of match in a string in VBA. In brief, it's a case of looping through your search string from start until the first instance of the search argument is located and replacing just that.

MS Word VBA - Finding a word and changing its style

I'm trying to find all instances of key words in a MS Word document and change their style. The key words are stored within an array and I want to change the style of the particular word only. Ideally this would happen as I type but that is not crucial.
Attempt 1 - Based on recording a macro and changing the search term
Sub Woohoo()
Dim mykeywords
mykeywords= Array("word1","word2","word3")
For myword= LBound(mykeywords) To UBound(mykeywords)
Selection.Find.ClearFormatting
Selection.Find.Replacement.ClearFormatting
Selection.Find.Replacement.Style = ActiveDocument.Styles("NewStyle")
With Selection.Find
.Text = mykeywords(myword)
.Replacement.Text = mykeywords(myword)
.Forward = True
.Wrap = wdFindContinue
.Format = True
.MatchCase = False
.MatchWholeWord = True
.MatchWildcards = False
.MatchSoundsLike = False
.MatchAllWordForms = False
End With
Selection.Find.Execute Replace:=wdReplaceAll
Next
End Sub
This changes the style of the entire paragraph where the words are in.
Attempt 2 - Based on this question here How can I replace a Microsoft Word character style within a range/selection in VBA?:
Sub FnR2()
Dim rng As Range
Dim mykeywords
mykeywords = Array("word1","word2","word3")
For nKey = LBound(mykeywords) To UBound(mykeywords)
For Each rng In ActiveDocument.Words
If IsInArray(rng, mykeywords(nKey)) Then
rng.Style = ActiveDocument.Styles("NewStyle")
End If
Next
Next
End Sub
This finds words that are in single lines but skips the words that are within a paragraph for some reason, e.g. it finds
Some text
word1
more text
but not
Some text before word1 means that the code above doesn't change the format
Word1 also isn't changed in this instance
Attempt 3 - AutoCorrect; not actually tried:
As an alternative I was thinking to use AutoCorrect. However I have more than 100 keywords and have no idea how to add this to the AutoCorrect list automatically (I'm fairly VBA illiterate). The other problem I would see with this approach is that I believe that AutoCorrect is global, whereas I need this only to work for a specific document.
I believe the reason why your macro isn't finding the words is due to the presence of leading or trailing blank spaces. Providing that you have already defined the style "NewStyle" changing your if statement in SubFnR2 from
If IsInArray(rng, mykeywords(nKey)) Then
to
If mykeywords(nkey) = LCase(Trim(rng.Text)) Then
Should solve the issue. By the way if you want to keep the style of the word depending on whether it is upper or lower case then remove the LCase part.
Edit:
I have included the sub with the modification below. I have tested it on the examples you gave (cut and pasted into word) and it changed the style for both instances word1.
Sub FnR3()
Dim rng As Range
Dim mykeywords
mykeywords = Array("word1", "word2", "word3")
Dim nkey As Integer
For nkey = LBound(mykeywords) To UBound(mykeywords)
For Each rng In ActiveDocument.Words
If mykeywords(nkey) = LCase(Trim(rng.Text)) Then
rng.Style = ActiveDocument.Styles("NewStyle")
End If
Next rng
Next nkey
End Sub
Ok, your document behaves has you described, I'm not quite sure why. I checked selecting the range and just the word was selected, but then the whole paragraph was formatted. I have modified the code to modify the selection, shown below. This did just change the word.
Sub FnR4()
Dim rng As Range
Dim mykeywords
mykeywords = Array("word1", "word2", "word3")
Dim nkey As Integer
For nkey = LBound(mykeywords) To UBound(mykeywords)
For Each rng In ActiveDocument.Words
Selection.Collapse
rng.Select
If mykeywords(nkey) = LCase(Trim(rng.Text)) Then
Selection.Style = ActiveDocument.Styles("NewStyle")
End If
Next rng
Next nkey
End Sub