Hack 43 Cross-Reference AutomaticallyUsing the Cross-reference dialog to insert references, particularly in a lengthy document, can be frustrating because it shows you only a few headings at a time. This hack shows you how to create references automatically, without a visit to the dialog. Whoever decided that the
Cross-reference dialog in Word
(Insert In many cases, creating a cross-reference means converting static text into a dynamic reference by selecting it, then replacing it with the corresponding item from the Cross-reference dialog (as shown in Figure 4-27). But since there's that nine-item limit, you're in for some serious scrolling if you need to make many references. In a Sisyphean spiral, the longer your document is, the more references you likely need, and the longer it will take to find each item in that teeny, tiny list. Figure 4-27. Only nine items at a time are visible in the Cross-reference dialogThis hack shows you two ways to use VBA to automatically create cross-references to headings. In each case, the selected text is compared to the headings in a document, trying to find a match and create a cross-reference. 4.18.1 Referencing the Way Word Does
The procedure shown in this section uses Word VBA's GetCrossReferenceItems method, which returns a list of potential reference targets in a document. Because Word continually updates and indexes this list, accessing it is very fast. This code runs significantly faster than the code in the next section, but that speed comes at a price: you're limited to creating cross-references to items that Word considers potential targets, such as headings that use one of the built-in heading styles. If you've also got a different kind of heading style in your document, such as SidebarTitle, those headings don't "count" as possible reference targets. Place this macro in the template of your choice [Hack #50]
and either run it from the Tools If your current selection includes more than one paragraph, the macro exits without taking any action. Sub InsertAutoXRef( ) Dim sel As Selection Dim doc As Document Dim vHeadings As Variant Dim v As Variant Dim i As Integer Set sel = Selection Set doc = Selection.Document ' Exit if selection includes multiple paragraphs If sel.Range.Paragraphs.Count <> 1 Then Exit Sub ' Collapse selection if there are spaces or paragraph ' marks on either end sel.MoveStartWhile cset:=(Chr$(32) & Chr$(13)), Count:=sel.Characters.Count sel.MoveEndWhile cset:=(Chr$(32) & Chr$(13)), Count:=-sel.Characters.Count vHeadings = doc.GetCrossReferenceItems(wdRefTypeHeading) i = 1 For Each v In vHeadings If Trim (sel.Range.Text) = Trim (v) Then sel.InsertCrossReference _ referencetype:=wdRefTypeHeading, _ referencekind:=wdContentText, _ referenceitem:=i Exit Sub End If i = i + 1 Next v MsgBox "Couldn't match: " & sel.Range.Text End Sub There are two important limitations to note about this code. First, if multiple headings match the selected text, the code creates a reference to the first match and ignores subsequent matches. This limitation is a problem if you have multiple headings with the same text, such as "The Code," used throughout this book. Second, the code offers no protection against creating a self-reference. If the match found by the code is the text you've selected, the reference that's created will replace the text it's supposed to reference, resulting in a broken reference, as shown in Figure 4-28. Figure 4-28. Self-referencing creates a broken referenceThis kind of inadvertent text deletion is also possible when creating references from the Word Cross-reference dialog. When you create a self-reference, Word displays a message telling you the reference is emptybut only after the text has been deleted. 4.18.2 A Better Way to Reference
This method won't match the speed offered by the code shown above, but its flexibility makes it a better starting point for hacking your own solutions. Rather than looking only at paragraphs that use one of Word's built-in heading styles, this code examines every paragraph in the document, looking for a match to the selected text. That means you can easily create a reference to a heading that uses a custom paragraph style, such as SidebarTitle. Unlike the code in the previous section, this procedure also checks to be sure the match it's found isn't the selected text, avoiding the possibility of a self-reference. This code is divided into five separate procedures: the MakeAutoXRef procedure and four supporting procedures, each of which performs an operation needed to create the reference. Place all five procedures in the template of your choice [Hack #50] and run the one named MakeAutoXRef to create a reference in place of the selected text. The first procedure, named MakeAutoXRef, is shown first. In conjunction with the supporting procedures shown afterward, it examines each paragraph in the document. If it finds one that matches the selected text, it creates a bookmark around the match and then replaces the selected text with a reference pointing to the bookmark. If the matched paragraph has already been referenced elsewhere, the existing bookmark is used. Sub MakeAutoXRef( ) Dim sel As Selection Dim rng As Range Dim para As Paragraph Dim doc As Document Dim sBookmarkName As String Dim sSelectionText As String Dim lSelectedParaIndex As Long Set sel = Selection Set doc = sel.Document If sel.Range.Paragraphs.Count <> 1 Then Exit Sub lSelectedParaIndex = GetParaIndex(sel.Range.Paragraphs.First) sel.MoveStartWhile cset:=(Chr$(32) & Chr$(13)), Count:=sel.Characters.Count sel.MoveEndWhile cset:=(Chr$(32) & Chr$(13)), Count:=-sel.Characters.Count sSelectionText = sel.Text For Each para In doc.Paragraphs Set rng = para.Range rng.MoveStartWhile cset:=(Chr$(32) & Chr$(13)), _ Count:=rng.Characters.Count rng.MoveEndWhile cset:=(Chr$(32) & Chr$(13)), _ Count:=-rng.Characters.Count If rng.Text = sSelectionText Then If Not GetParaIndex(para) = lSelectedParaIndex Then sBookmarkName = GetOrSetXRefBookmark(para) If Len(sBookmarkName) = 0 Then MsgBox "Couldn't get or set bookmark" Exit Sub End If sel.InsertCrossReference _ referencekind:=wdContentText, _ referenceitem:=doc.Bookmarks(sBookmarkName), _ referencetype:=wdRefTypeBookmark, _ insertashyperlink:=True Exit Sub Else MsgBox "Can't self reference!" End If End If Next para End Sub The code shown in bold is the part of the procedure that actually creates the reference. Note that it's very similar to part of the code shown in the previous section. 4.18.2.1 The supporting proceduresThe following function removes from a string characters that Word won't allow in bookmark names (except for spaces, which are replaced by underscores in a different procedure): Function RemoveInvalidBookmarkCharsFromString(ByVal str As String) As String Dim i As Integer For i = 33 To 255 Select Case i Case 33 To 47, 58 To 64, 91 To 96, 123 To 255 str = Replace(str, Chr (i), vbNullString) End Select Next i RemoveInvalidBookmarkCharsFromString = str End Function The next function takes a string and turns it into a valid bookmark name, including prefacing it with "XREF" for easier identification and adding in a five-digit random number [Hack #68] to ensure that it's unique. In addition, the function replaces underscores with spaces. So, for example, the heading "Foo the bar" would be converted into something like "XREF56774_Foo_the_bar"a bit easier to work with than the "_Ref45762234"-style names that Word assigns to its own cross-reference bookmarks. Function ConvertStringRefBookmarkName(ByVal str As String) As String str = RemoveInvalidBookmarkCharsFromString(str) str = Replace(str, Chr$(32), "_") str = "_" & str str = "XREF" & CStr(Int(90000 * Rnd + 10000)) & str ConvertStringRefBookmarkName = str End Function This next function just determines a paragraph's index in the document (e.g., the second paragraph in the document has an index of 2): Function GetParagraphIndex(para As Paragraph) As Long GetParagraphIndex = _ para.Range.Document.Range(0, para.Range.End).Paragraphs.Count End Function The final function creates cross-reference bookmarks in paragraphs that do not contain bookmarks and returns the bookmark name for use in the cross-reference. If the paragraph already has a cross-reference bookmark, it simply returns the existing bookmark name for use in the cross-reference. Function GetOrSetXRefBookmark(para As Paragraph) As String Dim i As Integer Dim rng As Range Dim sBookmarkName As String If para.Range.Bookmarks.Count <> 0 Then For i = 1 To para.Range.Bookmarks.Count If InStr(1, para.Range.Bookmarks(i).Name, "XREF") Then GetOrSetXRefBookmark = para.Range.Bookmarks(i).Name Exit Function End If Next i End If Set rng = para.Range rng.MoveEnd unit:=wdCharacter, Count:=-1 sBookmarkName = ConvertStringRefBookmarkName(rng.Text) para.Range.Document.Bookmarks.Add _ Name:=sBookmarkName, _ Range:=rng GetOrSetXRefBookmark = sBookmarkName End Function 4.18.2.2 Running the hackThis hack wouldn't be much of a time-saver if you had to go through a menu to run it. This code is most helpful when assigned to a keyboard shortcut. To assign a macro to a keyboard shortcut, select
Tools |