That bookmark *does* exist - vba

I'm trying to get to a specific point in my Word document from excel VBA. My ultimate goal is to copy new text into the bottom of the document.
So I put a bookmark at the bottom of my document and did this:
wDoc.Goto What:=wdGoToBookmark, Which:=wdGoToFirst, Name:="TemplatePage"
And it tells me "That bookmark does not exist." Which is funny, because it clearly does. It says the same thing about all my bookmarks.
This does not give me an error, instead it doesn't do anything:
wDoc.Goto What:=wdGoToBookmark, which:=wdgotofirst
Am I missing something obvious here?

Am I missing something obvious here?
IMO you are missing something, but the use of the word "Goto" can create the wrong impression.
.Goto actually makes the object that you specify "goto" the specified location. So if that object is a range, you shouldn't expect the Selection to change.
The other thing is that you have to omit the "which" parameter in this case. I don't know how you could determine that from the documentation - it's a question of trial and error with some of these range-related things.
Since the range implied by wDoc or wDoc.Content won't change anyway, you will lose any useful information immediately by executing the .Goto as a command. If you use it as a function, you get a range, so you can use, e.g.
Dim r As Word.Range
Set r = wDoc.Goto(What:=wdGoToBookmark, Which:=wdGoToFirst)
Debug.print r.start, r.end
Or if you want to select the resulting range, you can use
r.Select
But if you want to set a range
Dim r As Word.Range
Set r = wDoc.Bookmarks("TemplatePage)
is (IMO) simpler and you already have the code that actually selects it if that is what you need.

Ok, so apparently the GoTo simply doesn't work. I tried all sorts of variations on Page, Section, Bookmark, they either did nothing, gave errors, or even moved to the wrong place (selected text in the header!)
Instead, you can try:
wDoc.Bookmarks("TemplatePage").Select
Works like a champ.

Related

Batch add formatted autocorrects with VBA in Word

I use a long Excel spreadsheet containing incorrect and correct terms to check consistency between documents (e.g. anti-citrullinated is always hyphenated). I've added quite a few of these as autocorrect entries via the AutoCorrect Options feature in Word but it's time-consuming .
I came across the following code that will add long lists of autocorrects.
Sub BatchAddAutoCorrectEntries()
Dim objTable As Table
Dim objOriginalWord As Cell
Dim objOriginalWordRange As Range
Dim objReplaceWordRange As Range
Dim nRowNumber As Integer
Set objTable = ActiveDocument.Tables(1)
nRowNumber = 1
For Each objOriginalWord In objTable.Columns(1).Cells
Set objOriginalWordRange = objOriginalWord.Range
objOriginalWordRange.MoveEnd Unit:=wdCharacter, Count:=-1
Set objReplaceWordRange = objTable.Cell(nRowNumber, 2).Range
objReplaceWordRange.MoveEnd Unit:=wdCharacter, Count:=-1
AutoCorrect.Entries.Add Name:=objOriginalWordRange.Text, Value:=objReplaceWordRange.Text
nRowNumber = nRowNumber + 1
Next objOriginalWord
MsgBox ("All autocorrect items in the table1 are added.")
End Sub
It doesn't preserve any formatting: super- or subscripts, etc. Formatting autocorrect entries are stored in the Normal.dotm file and not in the regular .acl file so I haven't been able to figure out a way around this.
In a similar post, someone suggested a Find and Replace macro but Find and Replace doesn't allow me to replace with super- or subscripts.
There are two methods of adding Auto Correct Entries, Add and AddRichText. It is this second one that you use for formatted entries.
When faced with an issue like this my first resort is to check the Object Brower in the VBA editor (press F2 to display) to see what methods and properties may be available. My next step is to look them up in the VBA technical reference, aka Help, to check the usage.
If the problem is just sub/superscribt, then you could use uni-codes. Those are also available in autocorrect. Fx writing the unicodes ₁₂₃₄₅₆₇₈₉ instead of using formating on a normal 2. Most (but not all) characters exist in super and sub unicode.
The program is not working. It is giving an error message
Compile Error Expected Function or Variable
It is showing the following line as error
Autocorrect.Entries.Add Name:=objOriginalWordRange.Text, Value:=objReplaceWordRange.Text

Insert Building Blocks using Word VBA

I'm trying to insert a formatted table that I have saved in word named "DV Table" as part of the building blocks using VBA. I need this table to be inserted at the 13th paragraph of the word document.
Here's my code below. The first 3 lines just sets the selection to be at the 12th paragraph and create a new paragraph (13) after that. The last line of code is to insert the table. But when I run this, it gives the error.
Compile Error: Sub or Function not defined
I guess that this is not the proper way of defining the location. Would like some help on this. Thanks.
ActiveDocument.Paragraphs(12).Range.Select
Selection.EndKey Unit:=wdLine
Selection.Paragraphs.Add
ActiveDocument.AttachedTemplate.BuildingBlockEntries("DV Table" _
).Insert Where:=Paragraphs(13).Range.Select, RichText:=True
The Where parameter requires a Range object. There are two problems with Paragraphs(13).Range.Select
it's a method - it's an action, selecting something, not returning an object
Paragraphs(13) isn't "fully qualified" - VBA doesn't know what it is/what is meant.
One possibility would be
ActiveDocument.Paragraphs(13).Range
Notice ActiveDocument. preceding Paragraphs: this "fully qualifies" Paragraphs(13) - it tells VBA to what that belongs. And, since Where requires a Range object, Paragraphs(13).Range should be a correct "target" (I have not tested your code).
Generally, it's preferable not to work with Selection, just with Range objects. There's usually no need to actually select something using VBA. An alternative to the code snippet in the question could be
Dim rng As Word.Range
Set rng = ActiveDocument.Paragraphs(13).Range
rng.Collapse wdCollapseEnd 'like pressing right-arrow for a selection
rng.InsertParagraphAfter
rng.Collapse wdCollapseStart ' like pressing left-arrow for a selection
'rng.Select ' for testing / demo purposes
ActiveDocument.AttachedTemplate.BuildingBlockEntries("DV Table" _
).Insert Where:=rng, RichText:=True
In this case, the selection in the document does not change. There's no screen flicker; and code executes more quickly. This way of working takes getting used to, but once one is familiar with it, it's much easier to recognize what the code should be doing... Selection is rather vague as to what is being manipulated, especially if there's a lot of code using it.

Need help refining my excel macro for deleting blank rows or performing another action

Basically what I'm trying to accomplish is to search the document for blank rows and delete them, if any. This works great if there are blank rows to delete; however, if there are no blank rows, the macro ends with an error. I'd be eternally grateful if someone could advise me how to make this into an "if blank rows then this, if none then that"
Sheets ("xml") .Select
Cells.Select
Selection.SpecialCells(x1CellTypeBlanks).Select
Selection.EntireRow.Delete
Enter my second macro (this part works fine)
Regards
Let me point you to the canonical:
How to avoid using Select/Activate in Excel VBA macros
So you can start to understand why your current code fails or performs undesired operation. What happens when there are no blank cells in your selection? You'll get an error. Why?
Because in that circumstance, Selection.SpecialCells(xlCellTypeBlanks) evaluates to Nothing. (You can verify this using some debug statements) And because Nothing does not have any properties or methods, you'll get an error, because you're really saying:
Nothing.Select
Which is a null program, does not grok, does not compute, etc.
So, you need to test for nothingness with something like this:
Sheets("xml").Select
Cells.Select
If Not Selection.SpecialCells(x1CellTypeBlanks) Is Nothing Then
Selection.SpecialCells(x1CellTypeBlanks).EntireRow.Delete
End If
I still suggest avoiding Select at all costs (it is superfluous about 99% of the time and makes for sloppy code which is difficult to debug and maintain).
So you could do something more complete following that line of thought:
Dim blankCells as Range '## Use a range variable.
'## Assign to your variable:
Set blankCells = Sheets("xml").Cells.SpecialCells(xlCellTypeBlanks)
'## check for nothingness, delete if needed:
If Not blankCells Is Nothing then blankCells.EntireRow.Delete
Follow-up from comments
So in VBA we are able to declare variables which represent objects or data/values, much like a maths variable in an equation.
A Range is a type of object part of the Excel object model, which consists of the Workbook/Worksheets/Cells/Ranges/etc. (far more than I could hope to convey to you, here)
http://msdn.microsoft.com/en-us/library/office/ff846392(v=office.14).aspx
A good example of why to use variables might be here if you scroll down to the "Why Use Variables" section.
http://www.ozgrid.com/VBA/variables.htm
This is of course very simple... but the reader's digest version is that variables allow us to repeatedly refer to the same object (or value for sipmle data types) without explicitly referring to it each time.
THen there is the handy side-effect that the code bcomes more easy to read, maintain and debug, when we use variables instead of absolute references:
Dim rng as Range
Set rng = Sheets(1).Range("A1:Q543").Resize(Application.WorksheetFunction.CountA(Sheets(1).Range("A:A"),))
Imagine that fairly (but not ridiculously) complicated range construct. If you needed to refer to that range more than once in your code, it would be silly not to assign it to a variable, if for no other reason than to save your own sanity from typing (and possibly mistyping a part of it). It is also easy to maintain, since you need only modify the one assignment statement and all subsequent references to rng would reflect that change.

How to avoid default property gotchas in VBA?

I only use VBA occasionally, and every time I come back to it I get caught out by some variation of the following:
I have a Range object, currentCell, that I use to keep track of what cell I'm working with in the spreadsheet. When I update this to point to a different cell, I write:
currentCell = currentCell.Offset(ColumnOffset:=1)
The problem is that I've forgotten the Set keyword, so what the above line actually does is use the default property of the Range objects:
currentCell.Value = currentCell.Offset(ColumnOffset:=1).Value
So the contents of the current cell are overwritten by what's in the new cell, and my currentCell variable hasn't changed to point to a new cell, and I get filled with rage as I realize I've made the same mistake for the hundredth time.
There probably isn't a better answer than to put a post-it on my monitor saying "Have you remembered to use Set today?", but if anyone has any suggestions to help me, I'd appreciate hearing them. In particular:
Is there any way to turn on warnings when you implicitly use default properties? I have never used them like this on purpose, I'd always call Range.Value if that's what I meant.
Is there any good practice for marking variables as "this should only be used to read from the spreadsheet"? In most code I write, almost all my variables are for gathering data, and it would be handy to get a warning if something starts inadvertently editing cells like this.
It took me a while to understand how you are running into problem since I do not think I have ever had this problem in spite of using range objects for years. After thinking about things, I realized I do the following 100% of the time when working with cells - I use the offset or cells functions constantly - I rarely use Set to redefine a current variable.
If I have a loop I am iterating through to go through the spreadsheet, I may do something like
Dim startRng as Range
Set startRng = range("A1")
for i = 1 to 100
startRng.offset(i,0).value = i
startRng.offset(i,1).value = startRng.offset(i,0).value
next i
or
for i = 1 to 100
cells(i,0).value = i
cells(i,1).value = cells(i,0).value
next i
Either of these notations means I almost rarely have to use Set with a range object - almost always this happens once (if at all) and indicates the first cell in a range I will iterate over or reference.
It is also really clear what the offset is - since you specify a row/column - which makes it really straightforward what is happening in the code and easier to track since it references a single cell. You don't have to track down and trace backwards to the last 3 places you update a currentCell Range object.
Adopting a style of coding using these sorts of styles should eliminate nearly all these errors you are making. I am quite serious when I say I cannot remember ever having made a similar error in all my years coding VBA - I use the offset and cells functions continuously in my code (loops in these examples, but I use similar methods with all other examples in code) rather than setting ranges to new ranges. The side effect is that when you are setting a range in your code, it is almost ALWAYS immediately following a Dim statement and much more clear.
Whatever you choose to do, you'll need the post-it note, I'm afraid. After all, setting a range object's value to the value in another cell is a perfectly valid and common thing to do. The code has no way of knowing that you want it to do anything other than what you ask it to.
You could try checking your range object's address before and after you update it to make sure it's different, but if you remember to do that, you would be better off simply using the set keyword to update the object the way you intended.
Now that this issue has enraged you to the point of posting your question, I imagine that you'll never forget it again, regardless of how much time goes by before your next visit to VBA. So maybe you won't need the post-it note after all.
Is there any way to turn on warnings when you implicitly use default properties?
No.
Is there any good practice for marking variables as "this should only be used to read from the spreadsheet"?
Well, you could make your own variable naming convention, à la Making Wrong Code Look Wrong, but you'll still have to check your own code visually and the compiler won't help you do that. So I wouldn't rely on this too much.
A better option is to circumvent the need for repeatedly redifining currentCell using .Offset altogether.
Instead, read the entire range of interest to a Variant array, do your work on that array, and then slap it back onto the sheet when you're done modifying it.
Dim i As Long
Dim j As Long
Dim v As Variant
Dim r As Range
Set r = Range("A1:D5") 'or whatever
v = r.Value 'pull from sheet
For i = 1 To UBound(v, 1)
For j = 1 To UBound(v, 2)
'code to modify or utilise element v(i,j) goes here
Next j
Next i
r.Value = v 'slap v back onto sheet (if you modified it)
Voilà. No use of default properties or anything that could be confused as such. As a bonus, this will speed up your code execution.
There probably isn't a better answer than to put a post-it on my monitor saying "Have you remembered to use Set today?", but if anyone has any suggestions to help me, I'd appreciate hearing them. In particular:
I would slightly change the wording on the Post It
"Are you sure you have not forgotten using Option Explicit and Error Handling?"
Otherwise trust me there is no better way! Having said that, I would like to confirm that "Using Set" is the least of your worries. What should be on the top of your main worries is "Writing Good Code" and this doesn't come overnight. It all comes by practice.
My advice to all beginners. Never assume! For example, .Value is the default property of the range so
Range("A1") = "Blah"
is correct. But still avoid using that.
Always Fully qualify your variables
Always Use Option Explicit
Always Use Error handling
For example, This works.
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
On Error GoTo Whoa
Set ws = ThisWorkbook.Sheets("Sheet1")
Set rng = ws.Range("A1")
rng.Value = "Blah"
Exit Sub
Whoa:
MsgBox Err.Description
End Sub
Now let's try the above code without using the Set command. Try the below code. What happens?
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
On Error GoTo Whoa
ws = ThisWorkbook.Sheets("Sheet1")
rng = ws.Range("A1")
rng.Value = "Blah"
Exit Sub
Whoa:
MsgBox Err.Description
End Sub
Recommended Read.
Topic: To ‘Err’ is Human
Link: http://siddharthrout.wordpress.com/2011/08/01/to-err-is-human/
I think enderland has highlighted the basic solution, which is to handle processing multiple cells in a loop. To take it further, I'd suggest using a For Next loop for cycling through cells. Probably on of the most common bits of code I write is something like:
Dim cell as Excel.Range
Dim rngCellsToProcess as Excel.Range
Set rngCellsToProcess = 'whatever you set it to
For each cell in rngCellsToProcess
'do something
Next cell
This won't eliminate the need for Set, but may help remind you to use it, while making it clearer what's going on.
Maybe write your own custom function and use it instead ?
Sub offset_rng(ByRef my_rng As Range, _
Optional row As Integer, Optional col As Integer)
Set my_rng = my_rng.Offset(row, col)
End Sub
Can be used like this:
Sub test()
Dim rng As Range
Set rng = Range("A1")
offset_rng my_rng:=rng, col:=1
rng.Value = "test1"
offset_rng my_rng:=rng, col:=1
rng.Value = "test2"
offset_rng my_rng:=rng, col:=1
rng.Value = "test3"
offset_rng my_rng:=rng, col:=1
rng.Value = "test4"
End Sub

Clone variable in Word VBA

Given the range with revisions in it, I need to reconstruct the original text and the modified text. The first solution was to:
Sub OriginalText (ByVal Rng as Range)
Rng.Revisions.RejectAll
OriginalText = Rng.Text
End sub
Yet it turned out that ByVal is not really ByVal. the moment RejectAll is called, all changes are rejected in original document as well, and there is no way to apply them - they're gone from ThisDocument.Revisions.
Is there a (preferably handy) way to copy the variable Rng to any (say,Rng2) in the sub so I can work with the copy of the range without affecting the source?
Is there a way to serialized the range and bring it together, maybe?
Upd: Let me put it this way. Is there a chance to copy the object (Range in my case) so the changes made to the copy won't affect the source? I think that still is the fastest and the most elegant solution.
Range is a marker, even though it returns the content of the range. If you want the text, you need to say so.
Set r = Selection.Range
TextNow = Selection.Range.Text
TextOrig = OriginalText(r)
.RejectAll seems to be clearing all of the revisions (including when the text was first entered). It would be convenient if we could access .Revisions(0), but it doesn't look like this is possible.
Your best bet may be to see if you can store the original document before the changes are made. If that is not possible, you could go through with something like this script and find all of the changes by hand.
Try Range.Duplicate, which seems to create a clone of the range.