Anda di halaman 1dari 221

L P TRNH VBA FOR WORD

(Ti ng Anh)

Ngu n t li u Ng i su t m

: http://word.mvps.org/FAQs/MacrosVBA/index.htm : Phan T H ng

Ni pht hnh:

: www.giaiphapexcel.com

L p trnh VBA for Word CHAPTER 1: Beginners' tips.......................................................................................................... 10 1. How to modify a recorded macro ............................................................................................... 10 1.1. What's wrong with the recorder, anyway?.............................................................................. 10 1.2. Cleaning out unneeded dialog arguments............................................................................... 10 1.3. Making a macro more general ................................................................................................ 11 2. Making toggle macros.................................................................................................................. 13 2.1. Fixing broken Replace macros................................................................................................ 13 2.2. Naming and storing macros .................................................................................................... 14 2.3. More fun to come..................................................................................................................... 15 3. Creating a macro with no programming experience using the recorder................................ 15 4. Getting to grips with VBA basics in 15 minutes........................................................................ 17 5. Making the transition from WordBasic to VBA ....................................................................... 22 6. Organizing your macros .............................................................................................................. 23 6.1. Getting yourself organized ...................................................................................................... 23 6.2. Editing Macros........................................................................................................................ 24 6.3. Organizing your Global Templates......................................................................................... 24 7. When to use parentheses to enclose subroutine and function arguments............................... 26 8. The art of defensive programming ............................................................................................. 27 Recommended further reading. ...................................................................................................... 30 9. How to cut out repetition and write much less code, by using subroutines and functions that take arguments ................................................................................................................................. 31 10. How to use a single VBA procedure to read or write both custom and built-in Document Properties .......................................................................................................................................... 34 10.1. Writing Document Properties ............................................................................................... 34 10.2. Reading Document Properties .............................................................................................. 36 11. How to get the username of the current user........................................................................... 37 12. How can I get a list of the available printer names? ............................................................... 38 13. How to find out, using VBA, how many replacements Word made during a Find & Replace All....................................................................................................................................................... 40 14. Finding and replacing symbols ................................................................................................. 43 14.1. Basic Latin symbols listed under (normal text) ................................................................ 44 14.2. Upper Unicode characters and symbols which use decorative fonts.................................... 44 14.3. How to write your own macro to do the job.......................................................................... 45 15. Distributing macros to other users ........................................................................................... 48 CHAPTER 2: Returning information ............................................................................................ 50 1. How to check whether Word is open.......................................................................................... 50 2. Control Word from Excel............................................................................................................ 50 3. Determine whether the insertion point is located at the end of a document........................... 52 2 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 3.1. Start Property.......................................................................................................................... 52 3.2. End Property ........................................................................................................................... 52 3.3. Are We There Yet?................................................................................................................... 53 4. Detect whether a table cell is empty ........................................................................................... 54 4.1. Method 1.................................................................................................................................. 54 4.2. Method 2.................................................................................................................................. 54 4.3. Method 3.................................................................................................................................. 55 5. Determine the page number at the current cursor position..................................................... 55 6. Determine the number of pages in a document......................................................................... 55 7. How to get the column number of the selection (in a document containing snaking, or newspaper-style, columns)............................................................................................................... 56 8. Getting help with calling Word's built-in dialogs using VBA (and why doing so can be much more useful than you'd think) ......................................................................................................... 56 8.1. Where to find Help on them..................................................................................................... 56 8.2. Why use built-in dialogs?........................................................................................................ 56 9. Determine the position of the cursor on the page in points...................................................... 58 9.1. Important caveat ..................................................................................................................... 58 10. Determine whether the selection or range is at the start of a paragraph ............................. 59 11. Detect whether the first character in a selection is alphanumeric......................................... 59 12. How to find out whether the current document is running in another application (such as Internet Explorer, Outlook, etc.) .................................................................................................... 59 CHAPTER 3: Working with Bookmarks in VBA......................................................................... 61 1. Working with Bookmarks in VBA ............................................................................................. 61 1.1. Types of Bookmarks ................................................................................................................ 61 (1) Placeholder Bookmarks........................................................................................................ 61 (2) Enclosing Bookmarks........................................................................................................... 61 1.2. Inserting and retrieving text from a Bookmark....................................................................... 61 2. Inserting text at a bookmark without deleting the bookmark ................................................. 62 2.1. The problem............................................................................................................................. 62 2.2. The solution ............................................................................................................................. 63 3. How to create a menu to navigate to the non-hidden bookmarks in a document .................. 63 CHAPTER 4: Working with built-in dialogs ................................................................................ 66 1. Calling FileOpen dialog in VBA does not allow opening of multiple files .............................. 66 2. How to change the directory of the Save As dialog................................................................... 67 3. How to set the default suggested filename to be displayed by the Save As dialog the first time a user saves a new document........................................................................................................... 68 4. Passwords not saved when calling FileSaveAs dialog from VBA ............................................ 69 5. Force the user to save documents into a particular folder or a subfolder of that folder....... 70
Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 6. How to get the full path from the SaveAs dialog....................................................................... 71 7. Force the File New dialog to display in List view ...................................................................... 71 CHAPTER 5: Working with events ............................................................................................... 72 1. Running a macro automatically when Word starts or quits.................................................... 72 1.1. Global templates ..................................................................................................................... 72 2. Running a macro automatically when a document is created, opened or closed ................... 72 2.1. Using Document events........................................................................................................... 72 2.2. Using Auto macros .................................................................................................................. 73 2.3. Using Application Events ........................................................................................................ 73 3. Writing application event procedures ........................................................................................ 73 3.1. How to set up code that will respond to application events.................................................... 73 3.2. Summary of set-up procedure ................................................................................................. 76 3.3. oApp_Quit ............................................................................................................................... 77 3.4. oApp_DocumentChange.......................................................................................................... 77 3.5. oApp_DocumentBeforeClose, oApp_DocumentOpen, oApp_NewDocument, oApp_ WindowActivate, oApp_WindowDeactivate................................................................................... 78 3.6. oApp_DocumentBeforePrint, oApp_DocumentBeforeSave .................................................... 78 3.7. oApp_WindowBeforeDoubleClick, oApp_WindowBeforeRightClick, oApp_WindowSelectionChange ..................................................................................................... 78 4. How to create global event procedures similar to AutoOpen, AutoNew and AutoClose, without using Normal.dot ................................................................................................................ 78 1...................................................................................................................................................... 78 Using Word 97 ............................................................................................................................... 78 2...................................................................................................................................................... 81 Using Word 2000 and later versions.............................................................................................. 81 5. Intercepting events like Save and Print...................................................................................... 82 5.1. Intercepting commands ........................................................................................................... 82 5.2. Intercepting events (Word 2000 or later)................................................................................ 83 6. A Pseudo DocumentBeforeClose Event...................................................................................... 84 7. How can I prevent Word from running macros automatically when I create a new instance of Word, open a Word document or create a new one? ............................................................... 85 8. Assigning a macro to the tab key ................................................................................................ 86 9. Change the behavior of the TAB key inside a table cell ........................................................... 86 10. Prevent a file from showing up on the recently used files list ................................................ 89 11. How can I prevent users from editing the header of a document in Word 2000 or higher?90 CHAPTER 6: Working with properties......................................................................................... 91 1. Using VBA, how can I get access to the Document Properties of a Word file without opening the document?................................................................................................................................... 91

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 2. How to use a single VBA procedure to read or write both custom and built-in Document Properties .......................................................................................................................................... 91 2.1. Writing Document Properties ................................................................................................. 92 2.2. Reading Document Properties ................................................................................................ 93 3. Using VBA, how can I get access to the Document Properties of a Word file without opening the document?................................................................................................................................... 95 4. How to use a single VBA procedure to read or write both custom and built-in Document Properties .......................................................................................................................................... 95 4.1. Writing Document Properties ................................................................................................. 95 4.2. Reading Document Properties ................................................................................................ 97 5. Highlight any misspelled words, so that unrecognized words stand out prominently on a printout.............................................................................................................................................. 98 6. Clear all highlighting from a document ..................................................................................... 99 7. Turning Allow spacing between cells off with VBA in a Word 2000 table ......................... 99 7.1. Workaround 1.......................................................................................................................... 99 7.2. Workaround 2........................................................................................................................ 100 CHAPTER 7: Working with ranges and selections (not including Tables).............................. 101 1. How to select or set a Range object to the page that the cursor is on.............................. 101 2. Determine the index number of the current paragraph, table, section ................................ 101 1.................................................................................................................................................... 102 In order to operate on the currently selected paragraph, table, section, etc............................... 102 2.................................................................................................................................................... 102 Get the index number, by setting a range to the start of the document........................................ 102 3. How to move a range variable to the end of an inserted file after using [range].InsertFile 103 3.1. Workaround 1........................................................................................................................ 103 3.2. Workaround 2........................................................................................................................ 103 4. Delete any paragraph that is an exact duplicate of the preceding paragraph, using a Range object ............................................................................................................................................... 103 5. Delete any paragraph that is an exact duplicate of the preceding paragraph, using a Selection object ............................................................................................................................... 104 6. How to find out whether a range is off-screen......................................................................... 105 7. How to get the column number of the selection (in a document containing snaking, or newspaper-style, columns)............................................................................................................. 105 CHAPTER 8: Working with Tables............................................................................................. 106 1. Maximising the performance of Word tables .......................................................................... 106 As a user ....................................................................................................................................... 106 In code .......................................................................................................................................... 108 2. How can I resize a table to fit the page's width? ..................................................................... 114
Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 2.1. Word 2000 ............................................................................................................................. 114 2.2. Word 97 ................................................................................................................................. 114 3. Deleting duplicate rows in a table............................................................................................. 116 3.1. Method 1................................................................................................................................ 116 3.2. Method 2................................................................................................................................ 118 4. Detect whether a table cell is empty ......................................................................................... 118 4.1. Method 1................................................................................................................................ 118 4.2. Method 2................................................................................................................................ 119 4.3. Method 3................................................................................................................................ 119 5. Delete all rows of a table that contain a particular text string in the first column .............. 120 6. Determine the index number of the current paragraph, table, section ................................ 120 1.................................................................................................................................................... 120 In order to operate on the currently selected paragraph, table, section, etc............................... 120 2.................................................................................................................................................... 121 Get the index number, by setting a range to the start of the document........................................ 121 7. Apply changes to all cells in a table .......................................................................................... 122 8. Apply changes to an individual cell in a table ......................................................................... 122 9. Select all but the first two cells in a table column ................................................................... 122 10. Display in a message box the contents of each cell in a table column.................................. 123 11. Select a range of cells within a table ....................................................................................... 123 12. Select all rows of a table except the first row......................................................................... 123 13. How to centre a left-justified table (or left or right-justify a centred one) ......................... 124 14. How to get the Rowspan and Colspan of a table cell using VBA......................................... 124 14.1. Rowspan .............................................................................................................................. 124 14.2. Colspan................................................................................................................................ 125 15. Change the behavior of the TAB key inside a table cell ....................................................... 125 16. Turning Allow spacing between cells off with VBA in a Word 2000 table ..................... 128 16.1. Workaround 1...................................................................................................................... 128 16.2. Workaround 2...................................................................................................................... 128 CHAPTER 9: Working with strings, dates and Find & Replace .............................................. 129 1. How do I return the date of the previous month using VBA? ............................................... 129 2. Using a macro to replace text where ever it appears in a document ..................................... 130 2.1. Step 1 ..................................................................................................................................... 130 2.2. Step 2 ..................................................................................................................................... 131 2.3. Step 3 ..................................................................................................................................... 132 3. How to Find & ReplaceAll on a batch of documents in the same folder .............................. 133 4. Clear settings from Find and Replace dialog to prevent unexpected results from future Find or Replace operations..................................................................................................................... 136 6 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 5. Flush bad karma from Word's find facility after an unsuccessful wildcard search............ 137 6. How to prevent the built-in BrowseNext and RepeatFind commands from creating bad karma for wildcard searches ......................................................................................................... 138 6.1. Workaround 1: Intercepting the RepeatFind, BrowseNext and BrowsePrev commands ..... 138 6.2. Workaround 2: Executing the Find & Replace dialog to clear the bug ............................... 139 6.3.1. Notes regarding Workaround 2 ...................................................................................... 141 7. How to find out, using VBA, how many replacements Word made during a Find & Replace All..................................................................................................................................................... 142 8. Finding out how many times some text appears in a document ............................................ 145 9. How to use Edit Find to select everything from where the cursor is to the first found item145 9.1. Doing it programmatically.................................................................................................... 146 10. How to replace text in quotation marks with italic or highlighted text minus the quotes. 147 10.1. To do it manually................................................................................................................. 147 10.2. To do it with a macro .......................................................................................................... 148 11. Replace each instance of the text string Document One with the contents of a file called c:\test\Doc1.doc............................................................................................................................... 150 12. Replace one character with another wherever it appears in a string.................................. 151 13. Remove the underline attribute from characters with descenders...................................... 152 14. Remove all empty paragraphs from a document .................................................................. 152 CHAPTER 10: Working with files and directories..................................................................... 156 1. How to allow the user to browse to and select a folder........................................................... 156 2. How to Find & ReplaceAll on a batch of documents in the same folder .............................. 157 3. Skipping Password-Protected Documents in a Batch Process ............................................... 160 4. How to check if a file has already been opened by another user ........................................... 162 5. How to copy an open file using VBA ........................................................................................ 162 6. How to create a copy of an open document ............................................................................. 163 7. How to save a document using a filename that gets incremented by 1 each time if the filename already exists ................................................................................................................... 164 8. Insert into a document the names of all files in a selected folder........................................... 164 9. How to retrieve Word's default Documents path or Pictures path setting........................... 165 10. How to delete files using VBA, including files which may be readonly............................... 166 11. How to read the filenames of all the files in a directory into an array................................ 167 12. How to get the names of all the folders in the folder tree, starting from a specified folder .......................................................................................................................................................... 168 Notes............................................................................................................................................. 172 13. How to ensure (using VBA) that all your Word add-ins are installed in the correct path 172
Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word CHAPTER 11: Miscellaneous ....................................................................................................... 175 1. How can I get the mousewheel working in the VBA editing window?.................................. 175 2. How to do a mail merge to the printer using VBA, without displaying the Print dialog .... 176 3. How to do a screen capture using VBA.................................................................................... 177 3.1. Using SendKeys..................................................................................................................... 177 3.2. Using API calls...................................................................................................................... 177 4. How to get the username of the current user........................................................................... 178 5. Useful WordBasic commands that have no VBA equivalent ................................................. 180 5.1. SortArray............................................................................................................................... 180 5.2. FileNameInfo$() .................................................................................................................... 181 5.3. DisableAutoMacros............................................................................................................... 182 5.4. ToolsBulletsNumbers ............................................................................................................ 182 5.5. FileCopy/FileCopyA.............................................................................................................. 183 5.6. FileProperties........................................................................................................................ 184 5.7. SendKeys ............................................................................................................................... 184 6. How to send an email from Word using VBA ......................................................................... 184 6.1. Using the Routing Slip method.............................................................................................. 184 6.2. Automating Outlook .............................................................................................................. 185 7. How to get the most recently used document to be opened automatically when you open Word................................................................................................................................................ 187 1.................................................................................................................................................... 187 Using a command line switch to open the most recently used document .................................... 187 2.................................................................................................................................................... 188 Using a macro to open the most recently used document whenever Word opens ....................... 188 3.................................................................................................................................................... 189 Using a macro to open the most recently used document whenever Word is opened from its icon, but not when you open Word by launching a file......................................................................... 189 4.................................................................................................................................................... 190 The Application.GoBack bug and how to get round it................................................................. 190 8. Creating upside down or rotated text in Word ....................................................................... 190 8.1. Using two table cells ............................................................................................................. 190 8.2. Word 2002 only: Rotating a picture of your text .................................................................. 191 8.3. Pasting from PowerPoint into Word as a picture ................................................................. 191 8.4. Pasting from other vector graphics applications.................................................................. 192 8.5. Using Word Art ..................................................................................................................... 192 8.6. And just for the sake of completeness ................................................................................... 193 9. How can I tile documents vertically in Word 2000? ............................................................... 194 10. Invalid Page Fault message when running a macro.......................................................... 196 10.1. Obtaining the Word Code Cleaner...................................................................................... 197 11. Creating sequentially numbered documents (such as invoices)........................................... 198 12. Sequentially numbering multiple copies of single document using a macro ...................... 199 8 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 13. How to remove manually typed numbering from a document ............................................ 200 14. I want the numbers in my footnotes not to be superscripted, and I want the numbers to be followed by a dot and a tab............................................................................................................ 201 15. How to speed up Word Automation by hiding the application ........................................... 203 Chapter 12: Working with other objects and collections ........................................................... 206 1. How to convert the hyperlinks in a document to plain text ................................................... 206 Preserving hyperlinks within your Table of Contents.................................................................. 207 2. Size the text in a textbox to fill the textbox .............................................................................. 208 3. When I position a floating object (such as a text box or graphic) Relative to Page in Word 2000, it doesnt end up where it should why doesnt it? .......................................................... 209 4. Move shape anchors away from heading paragraphs ............................................................ 210 5. The simplest way, using VBA, to reset part of a style definition (e.g. the font name), so it inherits the definition of the style it is based on .......................................................................... 211 6. Cycle a paragraph through all available paragraph styles, eventually returning to the style the paragraph started with............................................................................................................ 213 7. How to safely update a document's styles from its template without using the Organizer (and how to make the Tools + Templates and Add-ins dialog safe) .......................................... 215 7.1. Overview of updating styles and template strategy............................................................... 215 7.2. When should styles be updated?............................................................................................ 215 7.3. Gotchas to be aware of, and their workarounds .............................................................. 216 If you want to be able to update the styles of Word 97 documents that are attached to Normal.dot................................................................................................................................ 219 8. Scroll all open documents the same percentage as the active document............................... 220

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word

CHAPTER 1: Beginners' tips 1. How to modify a recorded macro


Article contributed by Jay Freedman Lots of articles advise you to use the macro recorder to get started with Word macros. An example is our own Creating a macro with no programming experience using the recorder. Although it's good advice, the macros you record often need tweakingand sometimes they don't work at all.

1.1. What's wrong with the recorder, anyway?


The macro recorder's job is to translate your actions into programming code, using a language called Visual Basic for Applications (VBA). That's easy to do when you simply type some words into a document, or when you give a simple command such as Edit > Copy. It's harder for the recorder to make good code for more complicated commands such as File > Open or Edit > Replace. Sometimes the recorded code doesn't do exactly what you want it to do, and in a few situations the code is incorrect.

1.2. Cleaning out unneeded dialog arguments


You can record a macro while you make a change in the document by using a dialog, such as Format > Font or Format > Paragraph. You might think the macro recorder should capture only the settings that you changed, but it doesn'tinstead, it throws into the macro every setting in the whole dialog! Look at what you might get if you just change the font size to 10 pt:
Sub Macro1() With Selection.Font .Name = "Times New Roman" .Size = 10 .Bold = False .Italic = False .Underline = wdUnderlineNone .UnderlineColor = wdColorAutomatic .StrikeThrough = False .DoubleStrikeThrough = False .Outline = False .Emboss = False .Shadow = False .Hidden = False .SmallCaps = False .AllCaps = False .Color = wdColorAutomatic .Engrave = False .Superscript = False .Subscript = False .Spacing = 0 .Scaling = 100 .Position = 0 .Kerning = 0 .Animation = wdAnimationNone End With End Sub

Ni pht hnh: www.giaiphapexcel.com

10

L p trnh VBA for Word The lines that say "With Selection.Font" and "End With" mean that the lines between them set the properties of the font of the selected text. This is a shorthand way to refer to many properties of the same item, instead of writing "Selection.Font" at the beginning of each property's name. In this macro the one property that you changed during recording is buried among the many that you didn't change, which take extra storage and extra execution time. That's bad enough. But suppose you replay the macro after you select text that's in a different font or a different color, thinking that it will just change the size to 10 pt. In fact, the macro will also change the text to Times New Roman and Automatic color, because the recorder captured every setting in the dialog. That probably isn't what you intended the macro to do. Whenever you record the result of a dialog, you should inspect the code and delete all the extra settings. In this case, to apply only the size change, all you need is this:
Sub Macro1A() With Selection.Font .Size = 10 End With End Sub

There's now only one property being changed, so the "With Selection.Font" and "End With" are also unnecessary. The macro can be further simplified to this:
Sub Macro1B() Selection.Font.Size = 10 End Sub

If you aren't sure of the name of the property you need to keep, record a macro while you set a property to one value, and then record another macro while you set the same property to a different value. Compare the two macrosthe property you want to keep is the one whose value changes, and the others can be deleted from the revised macro.

1.3. Making a macro more general


Suppose you record a macro that opens a document and then does something to it, such as changing the view. The beginning of the macro may look like this: 1
Sub Macro2() Documents.Open FileName:= "Lorem.doc" , _ ConfirmConversions:= False , _ ReadOnly:= False , AddToRecentFiles:= False , _ PasswordDocument:= "" , PasswordTemplate:= "" , _ Revert:= False , WritePasswordDocument:= "" , _ WritePasswordTemplate:= "" , _ Format:=wdOpenFormatAuto, XMLTransform:= "" ' more code, for example ... ActiveWindow.View = wdPrintView End Sub

The recorded macro for opening a document, like the recording of the Format > Font dialog, contains unneeded things. In this case, they are parameters that contain information about the file, such as a password. The only parameter that's necessary is the FileName. You can remove the other parameters from the command, and Word will use its default values for them.
Ni pht hnh: www.giaiphapexcel.com

11

L p trnh VBA for Word A more important problem is that every time you run the recorded macro, it will open the same document. This may be what you intend, but more likely you want the macro to let you choose which document to open. One way to get the file's name into the macro is to display an input box, where you can type it in. The InputBox function shows a message box with a text entry field, and its result is the name that you type into the field.
Sub Macro2A() Dim MyFileName As String MyFileName = InputBox( "Enter file name to open:" , _ "Open a Document" ) If MyFileName <> "" Then Documents.Open FileName:=MyFileName ' more code, for example ... ActiveWindow.View = wdPrintView End If End Sub

But this isn't very friendly. There's a good chance of making a typing mistake. And if the document isn't in the currently active folder, you have to type the entire path to it. A better idea is to use the File > Open dialog that's already built into Word, which lets you browse to and select the proper document. When you click the OK button in the dialog, Word opens the selected document. The macro is simpler, too, because it doesn't need a separate Documents.Open statementthe dialog handles it all for you.
Sub Macro2B() If Dialogs(wdDialogFileOpen).Show = - 1 Then ' more code, for example ... ActiveWindow.View = wdPrintView End If End Sub

The word "Dialogs" in this code refers to a list of all of Word's built-in dialogs. Each dialog has a name that starts with "wdDialog". In this case, wdDialogFileOpen is the name of the built-in File > Open dialog, and the expression "Dialogs(wdDialogFileOpen)" selects that particular dialog from the list. To see all of the possible names, press F2 in the VBA editor to display the Object Browser, type wdDialog into the search box, and press Enter. The word ".Show" refers to a method of the dialog. A method is an action that can be donethe .Show method causes the dialog to appear and execute (carry out its function). Many methods also have a value after they execute, which tells the macro something about what just happened (this is called "returning" the value). In this case, if you click the OK button in the dialog then the .Show method returns the value 1, but if you click the Cancel button or the X in the title bar then .Show returns the value 0. The VBA help topic for each method tells you what values that method can return and what they mean. You can use the returned value in an If statement, as in Macro2B, to decide what to do. Similar changes to recorded code let you make macros that save files to variable locations, search for variable strings, and many other unrecordable variations.
Ni pht hnh: www.giaiphapexcel.com

12

L p trnh VBA for Word

2. Making toggle macros


The Italic, Bold, and Underline buttons on the toolbar are togglesclick the button once to turn it on, and again to turn it off. If you want to make your own toggle for something else, you can record separate macros for turning it on and off, but how do you combine them into one? As an example, let's make a macro to toggle the font's outline property on and off. If you record the change to turn it on, and remove the unnecessary properties, you get this:
Sub Macro3() With Selection.Font .Outline = True End With End Sub

One way to make a toggle macro from this code is to use an If statement. You test the current value and then assign the opposite value to the property:
Sub Macro3A() With Selection.Font If .Outline = False Then .Outline = True Else .Outline = False End If End With End Sub

A more efficient way is to use the Not operator. If the value is False, then applying Not to the value returns True, and vice versa. With this operator the macro can be written as
Sub Macro3B() With Selection.Font .Outline = Not .Outline End With End Sub

2.1. Fixing broken Replace macros


One of the most common actions to record is a Replace operation. A macro can be a great timesaver, since setting up the same Replace over and over can be time-consuming and it's easy to make a mistake. In one circumstance, though, the recorder creates a macro that simply doesn't work. Suppose you record the replacement of all italic text with the same text in bold italic. While you're recording this operation, it works perfectly well. If you replay the macro on another document, though, nothing happens! What's the matter? A look at the recorded code reveals the problem:
Sub Macro4() Selection.Find.ClearFormatting Selection.Find.Replacement.ClearFormatting

Ni pht hnh: www.giaiphapexcel.com

13

L p trnh VBA for Word


With Selection.Find .Text = "" .Replacement.Text = "" .Forward = True .Wrap = wdFindContinue .Format = True .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False End With Selection.Find.Execute Replace:=wdReplaceAll End Sub

There's no mention of italic or bold italic anywhere in this code. Except for the notation .Format = True (which tells Word to use formatting information about the .Text or .Replacement properties while searching or replacing), the recorder has completely missed the fact that you were replacing one format with another. To make this macro work as intended, you have to add these lines:
.Font.Italic = True .Replacement.Font.Bold = True

The first of these lines tells Word to search for italic text. The second line tells it to make the replacement text boldbecause it's already italic, it will become bold italic. Besides making the macro correct, I like to make it consistent. The "With" and "End With" statements are meant to replace the references to Selection.Find; that both speeds up the macro and makes it easier to read. You can pull the ClearFormatting and Execute statements inside the With clause as well, to get this code:
Sub Macro4A() With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = "" .Replacement.Text = "" .Font.Italic = True .Replacement.Font.Bold = True .Forward = True .Wrap = wdFindContinue .Format = True .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False .Execute Replace:=wdReplaceAll End With End Sub

2.2. Naming and storing macros


When you record a macro, Word suggests a name like Macro1. By default, it puts the macro in a module named NewMacros in the Normal.dot template. You should make it a habit to rename your
Ni pht hnh: www.giaiphapexcel.com

14

L p trnh VBA for Word macros to give them descriptive names, and macros that are useful enough to keep should be stored in a more organized manner. To rename a macro, all you need to do is change the word that follows "Sub" in the first line. For example, you could change the first line of Macro4A to Sub ItalicToBoldItalic() That name will appear in the Tools > Macro > Macros dialog, and you can tell what it does without having to look at the code. You can organize macros into modules, just as you organize files into folders. In the VBA editor, use the View menu to display the Project Explorer and the Properties pane. On the Insert menu, click Module and notice that a folder named Module1 appears in the Project Explorer. Click that folder, and change its name in the Properties pane. To move a macro from one module to another, cut its code from the editing pane, double-click the destination module in the Project Explorer, and paste the code into the destination's editing pane. Unfortunately, the Project Explorer doesn't support drag-and-drop movement of macros. You can move an entire module from one template to another by using the Organizer (Tools > Macro > Macros > Organizer). Macros that are useful for a specific type of document should be stored in modules in the template used to create that type of document. Macros that are generally useful should be stored in a global template, as explained in What do Templates and Add-ins store?

2.3. More fun to come


There are lots of other situations in which the macro recorder gives you code that's inefficient or doesn't do what you want or expect, and you should practice by improving it. As you learn more about macros, sometimes you'll find it useful to record an action just to discover what statements in VBA are involved. Then you can throw away the recorded macro and write good code of your own. There are times when you can't get the recorder to record anything useful because the commands you want to use are grayed out. Then you can usually find out more by looking at articles here, or asking questions in the VBA newsgroups.

3. Creating a macro with no programming experience using the recorder


Article contributed by Bill Coan Word's macro recorder can help you acquaint yourself with macros and with Office 97's vba programming language.
Ni pht hnh: www.giaiphapexcel.com

15

L p trnh VBA for Word Let's assume that you create several documents from scratch on a daily basis. After composing the text, you press Ctrl+Home to position the cursor at the start of the document. Then you click the Center tool on the Formatting toolbar to center the first paragraph of your document. Eventually, it occurs to you that you should be able to record a macro for these last actions. That is, a macro that will automatically position the cursor at the start of the document and center the first paragraph. Technically, this scenario may call for a solution involving paragraph styles. But for now let's create a macro solution and let's use the macro recorder to create it. Proceed as follows: Choose Macro|Record New Macro on the Tools menu (or simply double-click the REC button in the Status bar.) 2. Enter a macro name (sorry, no spaces or punctuation allowed). 3. Click the Assign Macro To Keyboard button. 4. Press Ctrl+F8, then click Assign and click Close. Word will display the Stop Recording toolbar, which lets you know that Word is recording your actions and will continue recording them until you click the Stop Recording button on this toolbar. (Don't click it yet!) 5. Press Ctrl+Home to position your cursor at the start of your document. 6. Click the Center tool on the Formatting toolbar. 7. Click the Stop Recording tool on the Stop Recording toolbar. 8. Open some other document. 9. Position the cursor in the middle of the document (doesn't matter where). 10. Press Ctrl+F8. 11. Smile with satisfaction as Word moves the cursor to the start of the document and centers the first paragraph. 1. So far, so good. Now to see what the macro looks like under the hood:

1. 2. 3.

Choose Macro|/Macros on the Tools menu. Select the macro that you just created. Click Edit.

Word will open up the vba editor and display something like the following macro:
Sub MyFirstRecordedMacro() ' ' MyFirstRecordedMacro Macro ' Macro recorded 10/31/98 by Elgin ' Selection.HomeKey Unit:=wdStory Selection.ParagraphFormat.Alignment = wdAlignParagraphCenter End Sub

4.

To learn more about the code, position the cursor somewhere in Selection.HomeKey and press 16

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 5. 6. F1 to view a related help topic. Close the help window and repeat Step 4 with the cursor in other locations. On the vba editor File menu, choose Close and Return to Microsoft Word.

If you have the patience to try these steps, you'll be well on your way. The rest is just details. There are lots of them but you can always search the vba help system and return to the newsgroups for additional information.

4. Getting to grips with VBA basics in 15 minutes


Article contributed by Bill Coan I can't turn you into a VBA expert but I can suggest a way to explore VBA that you may find helpful. Below, I've listed 22 steps that can be completed in approximately 15 minutes, assuming someone is kind enough to read them to you as you sit at your keyboard. If you have to read them by yourself and turn your attention alternately to the keyboard and back to the steps, then you may need a half hour or longer to complete the steps. Either way, the steps should give you a feel for what it's like to program in Word. Before starting, launch Word, then press Alt+F11 to launch the VBA editor, and then maximize the VBA editor window. Ready? Let's start: 1. In the VBA editor, choose Options on the Tools menu and make sure the following checkboxes are checked: Auto List Members Auto Quick Info Press F7 to view a code window, if not already displayed. Then type Sub Test and hit the enter key. The VBA editor will create a subroutine for you that looks as follows:

2.

Notice that the cursor is already flashing inside the subroutine, ready for you to type a command 3. Click in the code window and type the following, making special note of the dot at the end of the expression:
ActiveDocument.

If you want to work on the active document (and who doesn't) then you have to start this way. (Think Jesus: No one gets to the father except through me!) When you type the "." at the end of 17 Ni pht hnh: www.giaiphapexcel.com 4.

L p trnh VBA for Word the expression a list will pop up. This list is the most amazing guide you can imagine. Each item on the list is either a method or a property of the ActiveDocument or else it's an object unto itself that belongs to the ActiveDocument. For now let's deal with methods. Toward the end of this message we'll deal with properties. At the very end of the message, we'll deal with other objects besides the currently active document. A method is something you can DO to the ActiveDocument, like print it out. The way to think is this: I'm trying to do something to the active document as a whole, namely, print it. If I'm patient and scroll through this list, I'll almost certainly find a method that will do this for me. When you first think this way, the word method will stick in your throat like a fishbone. Later it will feel more like burnt toast and later still it will feel like candy when you were young. Indeed, in this case, if you're patient, you will scroll far enough to encounter a method called PrintOut. Eureka! You know it's a method (as opposed to a property) because it has an icon next to it that looks like a green brick flying through the air and at this stage the whole concept of method makes you feel like throwing a brick. Nice mnemonic, eh? Let's give it a try. Since you've scrolled down and selected PrintOut, you can press Tab to accept it or you can double-click it right there in the list. Either way, your statement now looks as follows

5. 6.

7.

So far so good. Most people easily get this far. But now what? One possibility is that you're done. After all, you've specified an object (the active document) and you've selected something that you want to do to it (print it out). Indeed, if you're willing to let Word print out the document in whatever way it chooses, then you are done. Another possibility is that you want to control how the print job will be carried out. In this case, you must provide some arguments (another fishbone, at first). Not to worry. To enter one or more arguments, all you do is type a space after the method and let the editor help you enter them. Arguments are equivalent to the choices you make in the Print dialog box. So go ahead, type a space after PrintOut, so your screen looks like this (the pipe character | represents your flashing insertion point after you've typed a space):

8.

But wait! When you type the space, the VBA editor suddenly gets very helpful again, showing you something like the following:

9.

Aha! Aha! These are the arguments for the PrintOut method. Most of them are immediately recognizable if you've ever paid attention to the Print dialog box. If you're wondering about one or more of them, simply press F1 and you'll call up a help topic that tells you all about each one of them. 10. Now that you can see all the arguments, you can enter values for as many of them as you desire.
Ni pht hnh: www.giaiphapexcel.com

18

L p trnh VBA for Word One way to do this is to type the name of the argument and then a value, using := to connect them and a comma to separate one argument/value from the next, like so:
ActiveDocument.PrintOut Background:=False, Copies:= 2

Another, boneheaded (in my opinion) way to do this is to enter values for ALL of the arguments, in which case you don't have to type the names of the arguments but you do have to account for ALL of them, as follows:
ActiveDocument.PrintOut False, , , , , , 2, , , , , , ,

11. Let's go with the named-argument approach: ActiveDocument.PrintOut Background:=False, Copies:= 2 12. That wasn't so painful, was it? Now press F5 to run the subroutine. Or return to Word and choose Tools|Macro|Macros...|Test|Run. (Pressing F5 is easier!) A quick review before we plunge on to properties. You think: I'm trying to do something to the active document as a whole, namely, print it. So I start by typing ActiveDocument. and a list pops up. I scroll through the list and select the PrintOut method. Then I type a space and enter some arguments. In this case, I want background printing off and I want two copies, so I type the names of those arguments and values for each of them. I connect each argument name to its value by using := because I'm part of the cognoscenti. Take a big breather here because now it's time to explore properties instead of methods . . . 13. Let's go back to our original assumption, namely, that you want to work on the active document as a whole. In this case, though, let's assume you want to change one of the properties of the document, rather than hit it with a brick. 14. Once again, click in a code window and type the following, making special note of the dot at the end of the expression: ActiveDocument. 15. Remember, if you want to work on the active document (and who doesn't) then you have to start this way. (No one gets to the father except through me!) When you type the . at the end of the expression a list will pop up. This list is the most amazing guide you can imagine. Each item on the list is either a method or a property of the ActiveDocument or else it's an object unto itself that belongs to the ActiveDocument. For now let's deal with properties. A property is a single characteristic. One of the properties of a document is its password. The way to think is this: I'm trying to change a property of the active document as a whole, namely, its password. If I'm patient and scroll through this list, I'll almost certainly find a password property. Indeed, in this case, if you're patient, you will scroll far enough to encounter a property called Password. Eureka! You know it's a property (as opposed to a method) because it has an icon next to it that looks like a finger pointing at a piece of information. Pretty useless mnemonic, eh? Let's give it a try. Since you've scrolled down and selected Password, you
Ni pht hnh: www.giaiphapexcel.com

19

L p trnh VBA for Word can press Tab to accept it or you can double-click it right there in the list. Either way, your statement now looks as follows:
ActiveDocument.Password

16. So far so good. Many people easily get this far. But now what? 17. In this case, the next step is to specify a value for the property. To do this, all you do is type a space and an equals sign after the name of the property, so your screen looks like this (the pipe character | represents your flashing insertion point after you've typed a space):
ActiveDocument.Password = |

18. Since the VBA editor has no idea what value you want to use for a password, it can't offer any suggestions. Instead, you simply have to come up with an idea on your own and type it in, perhaps as follows:
ActiveDocument.Password = "billcoan"

That's it! That's it! Now press F5 to run the subroutine. Or return to Word and choose Tools|Macro|Macros...|Test|Run. (Pressing F5 is easier!) When this statement runs, it will assign billcoan to be the password for the currently active document. You might wonder how you were supposed to know that the password had to be enclosed in quotation marks. Well, experience ought to be worth something, oughtn't it? In any case, if you had any question, all you would have had to do is position the cursor anywhere in the name of the property (Password) and press F1. This would display a help topic that tells you that a password requires a string, which is to say, a bunch of characters inside some quotation marks. Take a big breather here because now it's time to explore objects other than the currently active document . . . Let's face it, the currently active document, as a whole, can hold our attention for only so long. After all, you can carry out only so many methods on it (printout, save, saveas, etc., etc.) and you can change only so many of its properties (password, grammar checked, spelling checked, etc., etc.). But what about working on a particular part of a document, such as the first paragraph all by itself, or on a collection of parts, such as all the paragraphs? Here lies opportunity! After all, documents aren't just objects unto themselves; they're composed of hundreds of other, smaller objects. And you can work on each of those objects individually or as parts of collections. Let's dig deeper and find out how. 19. The good news is that you can reach any part of a document that you want to work on by starting out as though you were going to work on the document itself. In other words, you can start as you almost always start. That is, once again click in a code window and type the following, making special note of the dot at the end of the expression:
ActiveDocument.

20. If you want to work on a part of the active document then you have to start this way. (Remember: No one gets to the father except through me!) When you type the . at the end of 20 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word the expression a list will pop up. This list is the most amazing guide you can imagine. Each item on the list is either a method or a property of the ActiveDocument or else it's an object unto itself that belongs to the ActiveDocument. For now let's deal with objects that belong to the ActiveDocument. An object is something that you can work on by applying methods to it or by changing its properties. Word documents contain lots of different types of objects. When multiple objects of the same type exist (or can exist) in the same document, they are treated as collections. For example, a word document has, or can have, multiple paragraphs. Each paragraph is an object. All of the paragraph objects, together, form the paragraphs collection. The easiest document objects (and collections) to think about are paragraphs, words, and characters. The way to think is this: I'm trying to work on an object that belongs to the active document, namely a paragraph. Since a document can contain more than one paragraph, I'll have to locate the collection of paragraphs and then specify the specific paragraph that I want to work on. If I'm patient and scroll through this list, I'll almost certainly find the collection. When you first think this way, the words object and collection will stick in your throat like a fishbone. Later it will feel more like burnt toast and later still it will feel like love when you hit puberty. Indeed, in this case, if you're patient, you will scroll far enough to encounter a collection called Paragraphs. Eureka! You know it's not a method because it doesn't have an icon next to it that looks like a green brick flying through the air. You don't think of it as a property, either, but you quickly find out that the VBA editor *does* think of it as a property. OK, OK. So one of the properties of a Word document is that it contains a collection of paragraphs. Great. So Paragraphs is a collection of paragraph objects and Paragraphs is a property of a Word document. This is confusing, so quit worrying about it. Focus on the list! Find the item you want to work on! Let's give it a try. Since you've scrolled down and selected Paragraphs, you can press Tab to accept it or you can double-click it right there in the list. Either way, your statement now looks as follows:
ActiveDocument.Paragraphs

21. So far so good. Most people easily get this far. But now what? A major wrinkle, that's what! But hold on, it's easy to deal with. Since Paragraphs is a collection, you have to tell the VBA editor which paragraph you're interested in. You do this with a number in parentheses. For example, (1) refers to the first paragraph. Let's assume you want to work on the first paragraph in the collection. Type until your statement looks like this:
ActiveDocument.Paragraphs(1).

22. Guess what? You've just drilled down from the ActiveDocument object to the Paragraphs collection to the first Paragraph. That is, you've reached or specified an object that you want to work on. From here on out, life is easy. Why? Because working on a paragraph object is just like working on a document object. As soon as you type the dot after the object, the VBA editor shows you a list of all the methods, properties, and objects (or collections of objects) that belong to *your* object. Simply select the method that you want to carry out on your object, or select the property that you want to change, or keep drilling down by selecting 21

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word an object or collection that belongs to your object. That's all there is to it. A possible 23rd step would be to repeat Steps 1 -22 but replace all occurrences of ActiveDocument. with Selection. This allows you to drill down from the Selection object and discover the various methods, properties, and collections associated with the Selection object. A possible 24th step would be to repeat Steps 1 -22 but replace all occurrences of ActiveDocument. with Application. This allows you to drill down from the Word application object and discover the various methods, properties, and collections associated with that object.

5. Making the transition from WordBasic to VBA


Article contributed by Bill Coan The most difficult part about making the transition from WordBasic to VBA is learning to use Word's object model (instead of Word's user interface) as an organizing principle. As a WordBasic programmer, you could view macros as silent, behind-the-scenes users of Word. Indeed, if you read through a WordBasic macro, you find a series of commands that might just as well have come from a user. When Word 6 or 7 executed a macro, it behaved exactly as it would have behaved if a user were sitting at the keyboard and executing the commands manually. Sure, WordBasic offered looping and branching, but these were merely tools for managing the order in which commands were to be executed. The commands themselves might just as well have come from a user. As a Visual Basic programmer, you can view your macros as object manipulators. You can drill down to a particular object and then manipulate it. In the end, the types of manipulations that you can perform aren't all that different from what the user can accomplish via the user interface. But your macros can reach objects in more powerful and flexible ways than user interface commands could ever do. Moreover, your macros can reach those objects without regard to the current position of the selection. Plus, you can branch and loop in new, more powerful ways than you could with WordBasic. Consider the following statement, which allows you to manipulate the entire range of text found in the second row of the first table of the active document:
ActiveDocument.Tables(1).Rows(2).Range.Bold = True

Now consider this looping structure, which allows you to manipulate the same range within all tables in the document:
Dim oTable As Table For Each oTable In ActiveDocument.Tables

Ni pht hnh: www.giaiphapexcel.com

22

L p trnh VBA for Word


oTable.Rows(2).Range.Bold = True Next oTable

Powerful stuff, eh?

6. Organizing your macros


Article contributed by Beth Melton Many of us have a multitude of macros. I have too many to want them all cluttering up my menus, or to remember keyboard shortcuts for them all. Rather than having to look though your list of macros in the Macros dialog, or in the VB Editor, to find the one you want, the following methods may help to make it easier to find/edit/run the macro you want with a few clicks of the mouse; and if you have as many macros as me, will speed up Word as well.

6.1. Getting yourself organized


First categorize the macros into different Templates and Add-ins. Macros that are intended for use with specific document types should be placed in the template you base those documents on. I suggest creating at least two add-ins. One for those you frequently use and want to have loaded automatically when Word starts and one for those you don't use as often. If you want to further categorize the macros within the add-ins, you can create various modules: 1. 2. 3. 4. With your add-in open, go into the VB Editor (Alt+F11). In the Project pane, select the project (add-in). Select Insert/Module. Change the name of the Module in the Properties pane (if it isn't visible, press F4).

In each add-in, add each of the macro names and descriptions in the document itself, in a two column Word table. For each macro you can also include a MacroButton field so you can double-click it to run the macro:
{ MACROBUTTON EmailCleanup Run Macro }

For example: EmailCleanup Run Macro FolderContents To clean up email that has been copied/pasted into Word; removes > and | characters, manual line breaks, multiple spaces, etc. Creates a new document that lists the file names in a 23

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word Run Macro Figure 1 Or create a custom toolbar or menu in the add-in, that invokes the macros, as in Figure 2. See How to assign a Word command or macro to a toolbar or menu. Figure 2 specified folder, defaulting to current folder but displaying a dialog to let you choose the path.

6.2. Editing Macros


If you want a very quick way to get to the macro you want in order to edit it, store the following macro in Normal.dot, or in an add-in that automatically loads; it opens the VBA editor and takes you directly to the macro whose name you have selected in the current document:
Sub EditMacro() With Dialogs(wdDialogToolsMacro) .Name = Selection.Text .Edit = True . Execute End With End Sub

Add a custom button on one of your Toolbars so you can just double-click a macro name to select it and run the EditMacro macro. (If you do choose to utilize Normal.dot, keep a backup of it in the event it should become lost or corrupt. As an additional note, the same should be done with all of your valuable templates.)

6.3. Organizing your Global Templates


Global templates fall into three categories, Normal.dot; add-ins that automatically load when Word starts, and add-ins that you manually load as needed. To automatically load an add-in when Word starts, place either it, or a shortcut to it, in Word's Startup folder. This location can be obtained from Tools/Options/File Locations. Add-ins you wish to manually load can be placed in any folder except Word's Startup folder. I place all my add-ins in the same folder as each other, and put a shortcut in Word's Startup folder to those Add-ins that I want Word to load automatically. Only loading addins when they are needed can speed up Word. To manually load an add-in:

Ni pht hnh: www.giaiphapexcel.com

24

L p trnh VBA for Word 1. Go to Tools/Templates and Add-Ins. 2. Click the Add button and locate the add-in. When Word restarts they will still be in the list, just unloaded. When you need them all you have to do is go back to Tools/Templates and Add-Ins, and tick the appropriate one. In my setup, I also include a couple of templates that come with Microsoft Word as global templates too. Since I have several, I have created a UserForm to make loading them easier:

I have named my option buttons, respectively (from top to bottom): optWordMcr, optMacros and optSupport. Here are the necessary macros for this UserForm: In your project module:
Sub ShowGlobalTemplates() frmGlobal.Show End Sub

In your UserForm module:


Private Sub cmdCancel_Click() Unload Me End Sub Private Sub cmdOK_Click() If optWordMcr = True Then AddIns("C:\GlobalTemplates\Macros.dot").Installed = True ElseIf optMacros = True Then AddIns("C:\GlobalTemplates\MACROS9.DOT"). _ Installed = True ElseIf optSupport = True Then AddIns("C:\GlobalTemplates\SUPPORT9.DOT"). _ Installed = True End If Unload Me End Sub

Ni pht hnh: www.giaiphapexcel.com

25

L p trnh VBA for Word

7. When to use parentheses to enclose subroutine and function arguments


Article contributed by Jonathan West The rules are confusing concerning the use of parentheses to enclose argument lists. I have even seen MS Knowledgebase articles that have got it wrong. The rules are as follows. 1. For the initial line of a Sub or Function, you use parentheses to enclose the arguments (if any), e.g.
Sub MySubroutine(a As Long, b As String)

or
Function MyFunction(a As Long, B As String) As Long

2. When calling a function, you use parentheses to enclose the arguments, like this.
x = MyFunction(a:=1, b:="abc")

or
x = MyFunction(1, "abc")

3. When calling a sub directly, you don't use parentheses, like this.
MySub a:=1, b:="abc"

or
MySub 1, "abc"

4. When calling a sub using the Call keyword, you do use parentheses (this made sense to somebody!)
Call MySub(a:=1, b:="abc")

or
Call MySub(1, "abc")

Note that it doesn't matter whether or not you use the Call keyword to call a subroutine. The effect on the program is identical whether or not you use Call (assuming you have the parentheses right). Most of the code on this site doesn't use Call, simply because it is fewer words to type. 5. When dealing with methods of an object, use the same rules, depending on whether you are obtaining a return value from the method (like a function), or just applying the method to the 26

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word object without a return value (like a subroutine). For example, on the one hand:
Selection.GoTo What:=wdGoToPage, Name:="5"

But on the other hand (because you're now using a function which returns a value):
Dim MyRange As Range Set MyRange = ActiveDocument.Range Set MyRange = MyRange.GoTo(What:=wdGoToPage, Name:="5")

8. The art of defensive programming


Or how to write code that will be easy to maintain Article contributed by Jonathan West The source code that you write has two quite separate purposes. 1. It is a set of instructions to the computer, telling it to perform a particular task. 2. It is a description of the task and how you have gone about executing the task, for yourself and/or other programmers. This article is mainly about the second purpose, which a surprising number of people don't really think about. If you can't understand a program, then you can't debug it. Even with code that you have written yourself, if you come back to it six months or a year later, you may find yourself wondering Why on earth did I write that? What was it for? It doesn't take long to forget the details of a program when you aren't working on it any more. Make life easier for yourself, and write programs as clearly as possible. Also, provide such defences as you can against the possibility that VBA might change between versions of Word. The following example is based on a real question that came up in the newsgroups in 1999. The questioner wanted a macro that would do the following for a particular set of styles named AGR1 to AGR5:1. Check to see if the style already exists in the document. 2. If it does, then do nothing. 3. If it doesn't, then copy it from a central template. This is a fairly common kind of problem that Word VBA macros are excellent at solving. The following code was suggested in response to the question.
Dim StyleArray As Variant Dim StyleExists As Boolean Sub Numbering() With ActiveDocument Ni pht hnh: www.giaiphapexcel.com

27

L p trnh VBA for Word


.UpdateStylesOnOpen = False End With StyleArray = Array("AGR1", "AGR2", "AGR3", "AGR4", "AGR5") StyleExists = False For x = 0 To 4 For Each oStyle In ActiveDocument.Styles If oStyle = StyleArray(x) Then StyleExists = True End If Next oStyle If StyleExists = False Then Application.OrganizerCopy _ Source:="C:\Program Files\Microsoft Office\" & _ "Templates\Final Numbering TDS.dot", _ Destination:=ActiveDocument, Name:=StyleArray(x), _ Object:=wdOrganizerObjectStyles End If StyleExists = False Next x End Sub

Now, this code does the job, but there are lots of ways in which it could be made easier to read and understand, and less prone to future problems. Remember, source code isn't just there for the computer to execute. It is a message to yourself (or to the programmer who someday may have to come after you and maintain your code) as to what you were trying to achieve. If you make the code easy to read, then that helps you. Anyway, here is a set of improvements that could be made to the code. 1. There is no need for the Dim statements to be module-level declarations. They aren't needed in any other routine. Therefore they should come after the Sub Numbering() statement, so that there is no confusion with any other routine which might happen to use the same variable names. People tend to re-use favored and meaningful names, so this problem can be significant. 2. Numbering isn't a very meaningful name for the routine. It's always a good idea give a routine a name that clearly describes its purpose. Perhaps ImportAGRStyles would be better. 3. You should have the Require Variable Declaration checkbox set in the VBA Options dialog, so that there is an Option Explicit statement at the start of every module. That way, there is much less chance that VBA will interpret a typographical error in one of your variable assignments as the creation of an entirely new variable of type Variant. This being so, you would need Dim statements for x and oStyle, as Long & Style respectively. See also Why variables should be declared properly. 4. WithEnd With statements are a good idea, but there isn't really any need to use them if you are only putting one item in between. If you have two or more lines between them, then yes, great idea, use them whenever you can. 5. When updating the code at some point in the future, you might want to add another one or two items to StyleArray. It would be quite likely that you would forget (at least initially) to change the limit value of x, so that it reflects the new size of the array. It is much easier to let VBA to remember for you, by using UBound(StyleArray). That will adjust automatically to the number of items you include using the Array function. 6. A speedup trick. In the For Each oStyle loop, once you find a case where you set StyleExists = True, you don't need to go round the loop any more times. Therefore you can add an Exit For
Ni pht hnh: www.giaiphapexcel.com

28

L p trnh VBA for Word 7. statement to drop you out of the loop. Using If StyleExists = False Then is not very efficient. Better would be If Not StyleExists Then. In fact, for readability, better still would be to rename the variable StyleIsMissing and swap the True/False values round, so the line can now read If StyleIsMissing Then You can reset the value of StyleExists (or StyleIsMissing, as we would be renaming it) just once at the start of the loop, rather than once outside and once at the end of the loop. Fewer lines of code means fewer places that bugs can creep in! I don't trust default properties of objects. It would be too easy for MS to change them between versions of Word and break something in my code. Therefore I would prefer to use oStyle.NameLocal rather than just oStyle when comparing its value with StyleArray(x). We haven't been through enough versions of Word with VBA to find out whether this may be a common problem with MS, though a few things did change between Word 97 and Word 2000. I would simply prefer to minimize the risk, especially as I think it improves readability. Similarly, in the OrganizerCopy command, it would be better to use ActiveDocument.FullName instead of just ActiveDocument. Indenting the code so that it follows the structure of the IfThen statements and ForNext loops makes it much easier to understand where the loops and branches are. Also, it is a good idea to indent continuations of lines (perhaps by double the normal amount), so that it is quite clear that they are not new instructions. Also, it is common to indent everything except for comments and the lines which begin and end a routine. When a few lines of code together are needed for a specific task, keep them together, and then put a blank line below to show that you are starting on a new task. That way, the eye more clearly sees what your design is.

8.

9.

10.

11.

That's quite a few comments, they are longer than the code I've been discussing! However, with every line of code that you publish (by which I mean that will ever be seen by anyone other than yourself), you should be able to justify it as being about the best way it could be written. I have learned the need for this kind of coding discipline the hard way, by having to fix extremely nasty and obscure bugs. Also, I have learned that for longer running macros (though this particular macro should be over quickly) every speedup trick in the book is worth applying. The code with these changes would now look like this.
Option Explicit Sub ImportAGRStyles() Dim Dim Dim Dim StyleArray As Variant StyleIsMissing As Boolean x As Long oStyle As Style

ActiveDocument.UpdateStylesOnOpen = False StyleArray = Array("AGR1", "AGR2", "AGR3", "AGR4", "AGR5") For x = 0 To UBound(StyleArray) StyleIsMissing = True For Each oStyle In ActiveDocument.Styles If oStyle.NameLocal = StyleArray(x) Then StyleIsMissing = False Ni pht hnh: www.giaiphapexcel.com

29

L p trnh VBA for Word


Exit For End If Next oStyle If StyleIsMissing Then Application.OrganizerCopy _ Source:="C:\Program Files\Microsoft Office\" & _ "Templates\Final Numbering TDS.dot", _ Destination:=ActiveDocument.FullName, _ Name:=StyleArray(x), _ Object:=wdOrganizerObjectStyles End If Next x End Sub

Note that both versions of the routine do exactly the same thing. Maybe the second is marginally faster, but not to an extent that really matters. However, the second is much safer because the layout is clearer, the variables are local, and default properties are not used. It is no harder to write like this, once you get into the habit. For a short macro like this, perhaps it doesn't matter so much, but once you start writing macros, you will probably think of more complex tasks that you can automate with them. Then the defensive approach can make a big difference to the length of time it takes to get things right. There's one other thing to notice. I didn't put any comments in. For a routine that's as simple as this, if the code is written well, there shouldn't be any need for comments. At most, you might want a line or two at the start to describe the purpose of the routine. (You might also have other standard comments describing the author and revision level of the routine, but that's another issue altogether.) For a longer routine or a particularly obscure bit of code, it's probably a good idea to include a few extra comments at strategic points. These comments should aim to explain what you are trying to do, rather than how you are doing it. Once you know the what, the how should be clear from the code itself, and so doesn't need duplication.

Recommended further reading.


If you want a much fuller treatment of this subject, I would very strongly recommend that you buy a copy of Code Complete, by Steve McConnell, published 1993 by Microsoft Press, ISBN 1-55615484-4. The book was written before Word VBA existed, and even before Visual Basic existed, and so its examples are mainly in C, Pascal and some in BASIC. Despite this, it contains the best set of guidelines I have come across for writing good code in any language. It covers most of the points I have made in this article, in much greater depth than I possibly could here. The book is beautifully clear (just like code should be!), and you will have no difficulty following the examples, whichever language they are written in.

Ni pht hnh: www.giaiphapexcel.com

30

L p trnh VBA for Word

9. How to cut out repetition and write much less code, by using subroutines and functions that take arguments
Article contributed by Dave Rado Most of us write routines that do similar operations more than once. It makes your code much less cumbersome and much easier to follow if you hive off all such repetitive chunks of code into separate subroutines or functions. The difference between a sub and a function is that a function can return a value. Within the function itself, you can treat the function name like a variable, and give it a value and then you can call the function and get that value. Here's a ridiculously simple (and not very useful!) example:
Function GetResult As Long GetResult = 2 End Function Sub Test() MsgBox GetResult 'Returns 2 End Sub

But suppose you want to do a calculation 2 + 2. You could use:


MyResult = 2 + 2.

But alternatively, you could use a function to do the operation. The advantage is: less repetitive code (not so much in this example, because it's so simple, but in general). So you could have:
Function SumTwoValues(FirstNum As Long, SecondNum As Long) As Long 'Any 'and enclosed 'are sub or function can have variables passed to it, these variables, which need to be declared as shown here, in brackets called arguments

SumTwoValues = FirstNum + SecondNum End Function

And you can call the function like this:


Sub MainMacro() Dim MyResult As Long MyResult = SumTwoValues(2, 2) End Sub Ni pht hnh: www.giaiphapexcel.com

31

L p trnh VBA for Word That's exactly the same as MyResult = 2 + 2, but if the function contained more than just one line of code, and was called often, using a function like that would greatly reduce the amount of code needed and also make your code easier to follow. Let's take a more complex example. Supposing you had a macro that needs to do many Find and Replace operations, one after another. By using a subroutine that takes arguments to do the Find and Replaces it means you only need to have a single line of code for each and Replace. Here's a simple example: Let's suppose that all your Find & Replace operations are identical except for the find text and replacement text. Then you could have a sub or function that gets called, which looks something like this:
Sub DoFindReplace(FindText As String, ReplaceText As String) With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = FindText .Replacement.Text = ReplaceText .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False Do While .Execute 'Keep going until nothing found .Execute Replace:=wdReplaceAll Loop 'Free up some memory ActiveDocument.UndoClear End With End Sub

You can then call it like this:


Sub MainMacro() 'Remove double spaces Call DoFindReplace(" ", " ") 'Remove all double tabs Call DoFindReplace("^t^t", "^t") 'Remove empty paras (unless they folow a table or start or finish a doc) Call DoFindReplace("^p^p", "^p") 'etc etc End Sub Ni pht hnh: www.giaiphapexcel.com

32

L p trnh VBA for Word So only one extra line is needed for each Find and Replace operation. You can make it a bit more flexible by making any other parameters (such as .MatchCase) that might change from one find & replace operaration to the next into arguments that you can pass values to, instead of hard coding them. For instance, if .MatchCase is always set to False in your macro, you can just hard code it as shown above; but if it's True for some and False for others then you could use:
Sub DoFindReplace(FindText As String, ReplaceText As String, _ bMatchCase As Boolean) 'rest of sub as before except .MatchCase = bMatchCase End Sub

and you could call that like this:


Sub MainMacro() Call DoFindReplace("Ibm", "IBMI", True) Call DoFindReplace("laserjet", "LaserJet", False) End Sub

With Subs and Functions that take arguments as with Word's Methods (which are actually built-in functions) you can choose whether or not to specify the variable names when you call them. In the examples given so far I haven't used the variable names in the calling statements; and in the first few examples, there was no real point, because it's obvious what's going on in them. But in the last example, it's not so obvious what the third variable is (without looking at the function) so it's better to specify the variable names in this case, which you do like this:
Sub MainMacro() Call DoFindReplace(FindText:="Ibm", ReplaceText:="IBM", bMatchCase:=True) Call DoFindReplace(FindText:="laserjet", ReplaceText:="LaserJet", bMatchCase:=False) End Sub

That is much easier to follow, for anyone maintaining your code, than
Call DoFindReplace("Ibm", "IBMI", True) Call DoFindReplace("laserjet", "LaserJet", False)

Sometimes you might want to specify an argument in a sub or function, but you might want it to be optional. Most built-in Word functions include some optional arguments (such as the Background argument of the Printout method, for instance). To make an argument optional, you simply prefix it with the keyword Optional when you declare it, for example:
Ni pht hnh: www.giaiphapexcel.com

33

L p trnh VBA for Word


Sub DoFindOrReplace(FindText As String, Optional ReplaceText As String)

In this example, the procedure could check the value of the optional ReplaceText variable; and do a Find & Replace if it had a value, but a Find if it didn't. With optional arguments, you can also specify what value the variable should have if no value is specified. In other words you can specify its default value. If you do that, it's not optional in quite the same sense as before; it's actually mandatory, but you just don't have to specify its value in your code. If you don't specify its value, it will use the default value. So for example, you could have:
Sub DoFindReplace(FindText As String, ReplaceText As String, _ Optional bMatchCase As Boolean = False)

Then you'd only have to specify the value of the bMatchCase argument if you wanted it to be set to True. This can save a lot of typing!

10. How to use a single VBA procedure to read or write both custom and built-in Document Properties
Article contributed by Astrid Zeelenberg When you work with Document Properties in code, most people end up with two functions or subroutines, one to write built-in Document Properties and one for custom Document Properties; because in each case the object used to refer to the Document Properties is different you have to use the CustomDocumentProperties and BuiltinDocumentProperties collection as appropriate. But this can be very inconvenient.

10.1. Writing Document Properties


However, you can write a procedure which checks whether the property you want to write the value for is custom or built-in, and then uses the appropriate collection. (Note: If you are not familiar with calling subroutines with arguments, see: How to cut out repetition and write much less code, by using subroutines and functions that take arguments). This is how to do it:
Public Sub WriteProp(sPropName As String, sValue As String, _ Optional lType As Long = msoPropertyTypeString) 'In the above declaration, "Optional lType As Long = msoPropertyTypeString" means 'that if the Document Property's Type is Text, we don't need to include the lType argument 'when we call the procedure; but if it's any other Prpperty Type (e.g. date) then we do Dim bCustom As Boolean Ni pht hnh: www.giaiphapexcel.com

34

L p trnh VBA for Word


On Error GoTo ErrHandlerWriteProp 'Try to write the value sValue to the custom documentproperties 'If the customdocumentproperty does not exists, an error will occur 'and the code in the errorhandler will run ActiveDocument.BuiltInDocumentProperties(sPropName).Value = sValue 'Quit this routine Exit Sub Proceed: 'We know now that the property is not a builtin documentproperty, 'but a custom documentproperty, so bCustom = True bCustom = True Custom: 'Try to set the value for the customproperty sPropName to sValue 'An error will occur if the documentproperty doesn't exist yet 'and the code in the errorhandler will take over ActiveDocument.CustomDocumentProperties(sPropName).Value = sValue Exit Sub AddProp: 'We came here from the errorhandler, so know we know that 'property sPropName is not a built-in property and that there's 'no custom property with this name 'Add it On Error Resume Next ActiveDocument.CustomDocumentProperties.Add Name:=sPropName, _ LinkToContent:=False, Type:=lType, Value:=sValue If Err Then 'If we still get an error, the value isn't valid for the Property Type 'e,g an invalid date was used Debug.Print "The Property " & Chr(34) & _ sPropName & Chr(34) & " couldn't be written, because " & _ Chr(34) & sValue & Chr(34) & _ " is not a valid value for the property type" End If Exit Sub ErrHandlerWriteProp: Select Case Err Case Else 'Clear the error Err.Clear 'bCustom is a boolean variable, if the code jumps to this 'errorhandler for the first time, the value for bCustom is False If Not bCustom Then 'Continue with the code after the label Proceed Resume Proceed Else Ni pht hnh: www.giaiphapexcel.com

35

L p trnh VBA for Word


'The errorhandler was executed before because the value for 'the variable bCustom is True, therefor we know that the 'customdocumentproperty did not exist yet, jump to AddProp, 'where the property will be made Resume AddProp End If End Select End Sub

We could call the above procedure like this:


Sub Test() 'Author is a built-in property Call WriteProp(sPropName:="Author", sValue:="William Shakespeare") 'Date Updated is a custom document property Call WriteProp(sPropName:="Date Updated", sValue:="11 Mar 2001", _ lType:=msoPropertyTypeDate) End Sub

10.2. Reading Document Properties


The same principle can be used when reading Document Properties:
Function ReadProp(sPropName As String) As Variant Dim bCustom As Boolean Dim sValue As String On Error GoTo ErrHandlerReadProp 'Try the built-in properties first 'An error will occur if the property doesn't exist sValue = ActiveDocument.BuiltInDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ContinueCustom: bCustom = True Custom: sValue = ActiveDocument.CustomDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ErrHandlerReadProp: Err.Clear 'The boolean bCustom has the value False, if this is the first 'time that the errorhandler is runned If Not bCustom Then 'Continue to see if the property is a custom documentproperty Resume ContinueCustom Else Ni pht hnh: www.giaiphapexcel.com

36

L p trnh VBA for Word


'The property wasn't found, return an empty string ReadProp = "" Exit Function End If End Function

We could call the function like this:


Sub Test() Dim PropVal As String PropVal = ReadProp("Author") Debug.Print PropVal PropVal = ReadProp("Date Completed") Debug.Print PropVal End Sub

11. How to get the username of the current user


Article contributed by Astrid Zeelenberg If you want a routine that works for all types of networks, you'll have to use an API call to show the username of an user. The following code, which needs to be in a Module, was taken from Microsoft Knowledge Base article Q161394.
Option Explicit 'Declare for call to mpr.dll. Declare Function WNetGetUser Lib "mpr.dll" _ Alias "WNetGetUserA" (ByVal lpName As String, _ ByVal lpUserName As String, lpnLength As Long) As Long Const NoError = 0 'The Function call was successful

Function GetUserName() As String 'Buffer size for the return string. Const lpnLength As Long = 255 'Get return buffer space. Dim status As Integer 'For getting user information. Dim lpName, lpUserName As String

Ni pht hnh: www.giaiphapexcel.com

37

L p trnh VBA for Word


'Assign the buffer size constant to lpUserName. lpUserName = Space$(lpnLength + 1) 'Get the log-on name of the person using product. status = WNetGetUser(lpName, lpUserName, lpnLength) 'See whether error occurred. If status = NoError Then 'This line removes the null character. Strings in C are null'terminated. Strings in Visual Basic are not null-terminated. 'The null character must be removed from the C strings to be used 'cleanly in Visual Basic. lpUserName = Left$(lpUserName, InStr(lpUserName, Chr(0)) - 1) End If 'Display the name of the person logged on to the machine. GetUserName = lpUserName End Function

You could call it like this:


Sub Test() MsgBox GetUserName End Sub

Note that if the user is not logged in, the function will return nothing (an empty string).

12. How can I get a list of the available printer names?


Article contributed by Astrid Zeelenberg It requires some Windows API trickery, because VBA (unlike VB) does not have a Printers collection. Paste the following into a separate module. The function ListPrinters returns a variant containing an array of printer names.
Option Explicit Const PRINTER_ENUM_CONNECTIONS = &H4 Const PRINTER_ENUM_LOCAL = &H2 Private Declare Function EnumPrinters Lib "winspool.drv" Alias "EnumPrintersA" _ (ByVal flags As Long, ByVal name As String, ByVal Level As Long, _ pPrinterEnum As Long, ByVal cdBuf As Long, pcbNeeded As Long, _ pcReturned As Long) As Long Private Declare Function PtrToStr Lib "kernel32" Alias "lstrcpyA" _ Ni pht hnh: www.giaiphapexcel.com

38

L p trnh VBA for Word


(ByVal RetVal As String, ByVal Ptr As Long) As Long Private Declare Function StrLen Lib "kernel32" Alias "lstrlenA" _ (ByVal Ptr As Long) As Long

Public Function ListPrinters() As Variant Dim Dim Dim Dim Dim Dim Dim Dim Dim Dim bSuccess As Boolean iBufferRequired As Long iBufferSize As Long iBuffer() As Long iEntries As Long iIndex As Long strPrinterName As String iDummy As Long iDriverBuffer() As Long strPrinters() As String

iBufferSize = 3072 ReDim iBuffer((iBufferSize \ 4) - 1) As Long 'EnumPrinters will return a value False if the buffer is not big enough bSuccess = EnumPrinters(PRINTER_ENUM_CONNECTIONS Or _ PRINTER_ENUM_LOCAL, vbNullString, _ 1, iBuffer(0), iBufferSize, iBufferRequired, iEntries) If Not bSuccess Then If iBufferRequired > iBufferSize Then iBufferSize = iBufferRequired Debug.Print "iBuffer too small. Trying again with "; _ iBufferSize & " bytes." ReDim iBuffer(iBufferSize \ 4) As Long End If 'Try again with new buffer bSuccess = EnumPrinters(PRINTER_ENUM_CONNECTIONS Or _ PRINTER_ENUM_LOCAL, vbNullString, _ 1, iBuffer(0), iBufferSize, iBufferRequired, iEntries) End If If Not bSuccess Then 'Enumprinters returned False MsgBox "Error enumerating printers." Exit Function Else 'Enumprinters returned True, use found printers to fill the array ReDim strPrinters(iEntries - 1) For iIndex = 0 To iEntries - 1 'Get the printername strPrinterName = Space$(StrLen(iBuffer(iIndex * 4 + 2))) iDummy = PtrToStr(strPrinterName, iBuffer(iIndex * 4 + 2)) strPrinters(iIndex) = strPrinterName Next iIndex Ni pht hnh: www.giaiphapexcel.com

39

L p trnh VBA for Word


End If ListPrinters = strPrinters End Function

'You could call the function as follows: Sub Test() Dim StrPrinters As Variant, x As Long StrPrinters = ListPrinters 'Fist check whether the array is filled with anything, by calling another function, IsBounded. If IsBounded(StrPrinters) Then For x = LBound(strPrinters) To UBound(strPrinters) Debug.Print StrPrinters(x) Next x Else Debug.Print "No printers found" End If End Sub Public Function IsBounded(vArray As Variant) As Boolean 'If the variant passed to this function is an array, the function will return True; 'otherwise it will return False On Error Resume Next IsBounded = IsNumeric(UBound(vArray)) End Function

13. How to find out, using VBA, how many replacements Word made during a Find & Replace All
Or: How to find out how many occurrences there are of a particular word in a document Article contributed by Bart Verbeek and Dave Rado When you click Replace All in the Find & Replace dialog, Word shows the number of replacements on the Status bar after the operation is completed. To the regret of many it is impossible to query this number in VBA. But that does not mean you cannot determine the number of replacements if you want to. The following VBA code sample does just that. (If you are not familiar with using
Ni pht hnh: www.giaiphapexcel.com

40

L p trnh VBA for Word functions with arguments, see How to cut out repetition and write much less code, by using subroutines and functions that take arguments).
Function CountNoOfReplaces(StrFind As String, StrReplace As String) Dim NumCharsBefore As Long, NumCharsAfter As Long, LengthsAreEqual As Boolean Application.ScreenUpdating = False 'Check whether the length of the Find and Replace strings are the same; _ if they are, prefix the replace string with a hash (#) If Len(StrFind) = Len(StrReplace) Then LengthsAreEqual = True StrReplace = "#" & StrReplace End If 'Get the number of chars in the doc BEFORE doing Find & Replace NumCharsBefore = ActiveDocument.Characters.Count 'Do the Find and Replace With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = StrFind .Replacement.Text = StrReplace .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = True .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False .Execute Replace:=wdReplaceAll End With 'Get the number of chars AFTER doing Find & Replace NumCharsAfter = ActiveDocument.Characters.Count 'Calculate of the number of replacements, 'and put the result into the function name variable CountNoOfReplaces = (NumCharsBefore - NumCharsAfter) / _ (Len(StrFind) - Len(StrReplace)) 'If the lengths of the find & replace strings were equal at the start, _ do another replace to strip out the # If LengthsAreEqual Then StrFind = StrReplace 'Strip off the hash Ni pht hnh: www.giaiphapexcel.com

41

L p trnh VBA for Word


StrReplace = Mid$(StrReplace, 2) With Selection.Find .Text = StrFind .Replacement.Text = StrReplace .Execute Replace:=wdReplaceAll End With End If Application.ScreenUpdating = True 'Free up memory ActiveDocument.UndoClear End Function

You could call it like this:


Sub Test() MsgBox "Number of replacements: " & CountNoOfReplaces _ (StrFind:="Big", StrReplace:="Bigger"), vbInformation End Sub

This will work regardless of which string is the longest, and even if the strings do not differ in length. You could take the same principle further, to count the number of occurrences of a particular word in a document:
Function CountWord(WordToCount As String) Dim NumCharsBefore As Long, NumCharsAfter As Long Application.ScreenUpdating = False 'Get the number of chars in the doc BEFORE doing Find & Replace NumCharsBefore = ActiveDocument.Characters.Count 'Do the Find and Replace With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = WordToCount .Replacement.Text = "#" & WordToCount .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = True .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False Ni pht hnh: www.giaiphapexcel.com

42

L p trnh VBA for Word


.Execute Replace:=wdReplaceAll End With 'Get the number of chars AFTER doing Find & Replace NumCharsAfter = ActiveDocument.Characters.Count 'Calculate of the number of replacements, 'and put the result into the function name variable CountWord = NumCharsAfter - NumCharsBefore 'Undo the replace ActiveDocument.Undo 'Free up memory ActiveDocument.UndoClear Application.ScreenUpdating = False End Function

You could call it like this:


Sub Test() Dim Response As String Response = InputBox("Type a word you want to count", _ "Get number of occurrences of this word") MsgBox "There are " & CountWord(Response) & _ " occurrences of the word '" & Response & _ "' in this document", vbInformation End Sub

Note: although it is possible to achieve the same ends by using a counter while you do multiple Finds (or multiple Find & Replaces) one at a time, until nothing more is found, that method is much slower than doing a ReplaceAll as illustrated above.

14. Finding and replacing symbols


Article contributed by Dave Rado When it comes to performing Find and Replace operations, symbols listed in the Insert + Symbol dialog are a nightmare to work with in Word 97 and above unless, that is, you stick exclusively to using Unicode characters, and never use decorative fonts such as Wingdings or Symbol. But with most of the fonts that are in every day use, doing that puts quite a severe limitation on you, and in any case, documents you are emailed will often contain characters formatted with decorative fonts.
Ni pht hnh: www.giaiphapexcel.com

43

L p trnh VBA for Word

14.1. Basic Latin symbols listed under (normal text)


It's easy to find and replace characters such as , , , or any character listed in the Symbol dialog with the Font set to (normal text) and the Subset set to Basic Latin, (or to put it another way, any character included in the ANSI character set). Just insert the character(s) into your document and then cut and paste them into the dialog. The easiest way is to insert both the find and replace strings into your document first, next to each other, so you can paste both using Ctrl+V into both the Find what and Replace with boxes and delete as appropriate. So if you want to replace Andre with Andr, type AndreAndr, paste that into both boxes of the dialog, and delete Andr from the first box and Andre from the second. If you want to do it programmatically, turn the macro recorder on while performing these operations.

14.2. Upper Unicode characters and symbols which use decorative fonts
When it comes to upper Unicode characters such as: ... or symbols from decorative fonts such as Symbol or Wingdings, things become very messy. For example, if you apply the Wingdings font to the letter q, you get a square bullet. If you then search for the letter q even if you specify that the font should be Wingdings Word won't find the bullet symbol! Similarly, if you apply the Symbol font to the letter d, you get the Delta symbol, which Word will then not find if you search for the letter d. In Word 97, if you highlight your square bullet symbol and paste it into the Find dialog, Word will find it. But in Word 2000 it won't!!! If, in Word 97, you record the above Find operation using the macro recorder, it turns out Word 97 was searching for the Unicode character 61553. If you play back the macro in Word 2000, searching for Unicode 61553, Word 2000 will find the character. But it will also find (in both versions of Word) any other character whose Unicode number happens to be 61553. So using the Unicode number is not a wholly reliable method. In the specific case of decorative fonts included in the Insert + Symbol dialog (such as Symbol and Wingdings), you can use the Microsoft-supplied FindSymbol macro included in Macros9.dot (which works with both versions of Word). Unfortunately, the FindSymbol macro does not work with special characters from the (normal text) character set displayed in the Insert + Symbol dialog (or with the characters displayed on the Special Characters tab of the dialog). As you will see further down, there is no reason why it shouldn't it was just laziness on the part of the MS programmers. Also, if you want to write your own Find and Replace macros, to find and/or replace specific symbols, the Microsoft macro is too unwieldy to be of much use to you.
Ni pht hnh: www.giaiphapexcel.com

44

L p trnh VBA for Word In the case of upper Unicode characters displayed under (normal text), you can paste them into the Find dialog (in Word 2000 as well as Word 97); and unless you happen to have a symbol in the document from a decorative font that has the same Unicode number, that method will work reliably. But if you want to be safe, you will need to use a macro. Fortunately, such a macro is relatively straightforward to write even if you are new to VBA.

14.3. How to write your own macro to do the job


Finding symbols

The first thing you need to do is find out both the font and Unicode number of the character you want to search for and /or replace with. If the character was inserted from the Insert + Symbol dialog, its name won't be displayed in the Fonts list on your toolbar, but you can get at the information by selecting the character and running the following macro:
Sub GetCharNoAndFont() With Dialogs(wdDialogInsertSymbol) Debug.Print "Font: " & .Font Debug.Print "Char number " & .CharNum End With End Sub

Press Ctrl+G or select View + Immediate Window to see the results. In the case of the Delta symbol, that will return:
Font: Symbol Char number -3996

You can now use the following macros to find the next instance of the Delta symbol (if there is one). As you'll see shortly, it is very straightforward, even if you are a complete programming novice, to customise the following macros for your needs.
Sub FindDeltaSymbols() 'Call the main "FindSymbols" macro (below), 'and tell it what character code and font to search for Call FindSymbols(FindChar:=ChrW(-3996), FindFont:="Symbol") End Sub Sub FindSymbols(FindChar As String, FindFont As String)

Dim FoundFont As String, OriginalRange As Range, strFound As Boolean Application.ScreenUpdating = False 'set range to return to in case symbol not found Set OriginalRange = Selection.Range strFound = False With Selection.Find Ni pht hnh: www.giaiphapexcel.com

45

L p trnh VBA for Word


.ClearFormatting .Text = FindChar .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False Do While .Execute 'Keep going until nothing found If Dialogs(wdDialogInsertSymbol).Font = FindFont Then 'If the correct character was found, exit loop strFound = True Exit Do Else 'Otherwise search again Selection.Collapse wdCollapseEnd End If Loop If Not strFound Then 'if nothing found, search from the beginning of the document ActiveDocument.Range(0, 0).Select Do While .Execute If Dialogs(wdDialogInsertSymbol).Font = FindFont Then strFound = True Exit Do Else Selection.Collapse wdCollapseEnd End If Loop End If End With If Not strFound Then OriginalRange.Select End If Set OriginalRange = Nothing Application.ScreenUpdating = True End Sub

To customise it, all you have to do is to use the GetCharNoAndFont() macro again, to find out what chaacter number and font you need to use; and then change the character number and font used in the FindDeltaSymbols() macro to the correct values for the symbol(s) you want to find.

Ni pht hnh: www.giaiphapexcel.com

46

L p trnh VBA for Word For example, if you want to find the one-eighth symbol, which is listed under (normal text) in the insert symbol dialog, insert the symbol into your document, select it, and run the GetCharNoAndFont() macro. This will return:
Font: (normal text) Char number 8539

So just by changing the reference to Symbol in the FindDeltaSymbols() macro to (normal text) instead; and the reference to -3996 to 8539; we get the following:
Sub FindOneEigthSymbols() Call FindSymbols(FindChar:=ChrW(8539), FindFont:= "(normal text)") End Sub

Use this with exactly the same FindSymbols() macro as before. Finding and replacing symbols

If you want to do a Find and Replace operation, you will need the character numbers and fonts for both the Find and the Replace characters. As before, you can use the GetCharNoAndFont() macro to obtain this information. You can then customise the following first of the following macros, which calls the second macro, and in this example replaces all instances of the Delta symbol in your document with Beta symbols. To customise it, just change the character numbers and fonts in the ReplaceAllDeltaSymbolsWithBetaSymbols() macro (but leave the ReplaceAllSymbols() macro as it is):
Sub ReplaceAllDeltaSymbolsWithBetaSymbols() 'Call the main "ReplaceAllSymbols" macro (below), 'and tell it which character code and font to search for, and which to replace with Call ReplaceAllSymbols(FindChar:= ChrW(-3996), FindFont:= "Symbol", _ ReplaceChar:=-3998, ReplaceFont:="Symbol") End Sub Sub ReplaceAllSymbols(FindChar As String, FindFont As String, _ ReplaceChar As String, ReplaceFont As String) Dim FoundFont As String, OriginalRange As Range, strFound As Boolean Application.ScreenUpdating = False Set OriginalRange = Selection.Range 'start at beginning of document ActiveDocument.Range(0, 0).Select strFound = False With Selection.Find .ClearFormatting .Text = FindChar Ni pht hnh: www.giaiphapexcel.com

47

L p trnh VBA for Word


.Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False Do While .Execute 'keep searching until nothing found If Dialogs(wdDialogInsertSymbol).Font = FindFont Then 'Insert the replacement symbol where the found symbol was Selection.InsertSymbol Font:=ReplaceFont, _ CharacterNumber:=ReplaceChar, Unicode:=True Else Selection.Collapse wdCollapseEnd End If Loop End With OriginalRange.Select Set OriginalRange = Nothing Application.ScreenUpdating = True End Sub

15. Distributing macros to other users


Article contributed by Jonathan West So, you are working in a company and you have written some neat macros that make Word easier to use. Now the boss comes along and says I want you to set up all the PCs in the department with that. Ouch! How to do that? First of all, you need to know where the macros are to start with. Unless you have taken specific action to avoid this, all your macros are probably in a template called normal.dot (see: What do Templates and Add-ins store? for more on this). Your first thought is probably simply to copy your normal.dot to everyone, replacing theirs. Wrong answer! Normal.dot is Word's scratch-pad where many user settings get stored. This includes any macros they have written, any extra menu items or toolbars, keyboard shortcuts, AutoText entries etc. If you mess with those, you are liable to have a riot on your hands. Leave normal.dot well alone.
Ni pht hnh: www.giaiphapexcel.com

48

L p trnh VBA for Word There are two common ways in which macros get used.

1. Macros that are associated with a particular document type, and perhaps have to run automatically when documents of that type are opened or created. 2. Macros that need to be generally available at all times. For the first category, you need your macros to go into the template that those documents are based on. Copy the code to the template. Once you are sure it is working right, you can distribute that template to the users, and put it in their templates folder. For the second category, you do much the same. Copy the code into a fresh template, along with any additional menu items, toolbars and keyboard shortcuts that you want to set up to make it easier to access the macros. Once you are sure that it is all working, you can distribute that template. This one wants to go into the Word Startup folder. You can find out which folder this is by going to Tools, Options, File Locations, and checking the Startup folder. If the template is copies there, then next time the user opens Word, the macros and toolbars will all be available, because the template is automatically loaded as an add-in. How can you then distribute any changes you make to your macros in an automated way? The easiest way (which could be used when you first distribute the templates) is for your IT department to set up a login script that distributes any changes made to your templates and add-ins when users login. They can do this easily, by creating a batch file, containing an xcopy command, and getting the login script to call the batch file. An example of such an xcopy command might be:
XCOPY [server path] [local path] /D /Q /Y /R /I /C /K /S

This copies only new files or files that have changed, without prompting, and overwrites readonly files (they should be readonly); and also copies any new subdirectories, if you create any. Once this is set up, you only need to maintain the server copies, and changes will be distributed automatically. Your IT department can set up user profiles to ensure that everyone's template path is the same. (Obviously, if they are doing so for the first time, users will need to be warned)! Some companies point their users' Workgroup Template Path and their Startup path to a location on the server (using user profiles); but this doesn't cater for laptop users, whereas copying changes to users' hard disks does.

Ni pht hnh: www.giaiphapexcel.com

49

L p trnh VBA for Word

CHAPTER 2: Returning information

1. How to check whether Word is open


Article contributed by Lutz Gentkow Some users are very talented at opening many instances of Word, by repeatedly clicking on the shortcut, until memory or resources run out. This is particularly so with Word 97, where clicking on Word's shortcut almost always starts a new instance of Word. With Word 2000, clicking on Word's shortcut generally creates a new document using an existing instance of Word, if there is one; but if a Word dialog box is active at the time, even Word 2000 creates a new instance of Word. So I have written the following macro to prevent users from doing this. Being an AutoExec macro, it fires automatically when you start a new instance of Word; and if it finds two open instances (the new one plus another one that was already open), then it activates the one that was already open and quits the new instance.
Sub AutoExec() Dim Hits As Long, oTask As Task For Each oTask In Tasks If Left(oTask.Name, 14) = "Microsoft Word" Then Hits = Hits + 1 If Hits = 2 Then oTask.WindowState = wdWindowStateMaximize Application.Quit End If Next oTask End Sub

However, if you are using Automation to open Word from another application (such as a VB application, or Excel) and want to check whether Word is already open before you start, then you can't use the Tasks collection.

2. Control Word from Excel


Article contributed by Bill Coan and Dave Rado

Here's some code which uses Early Binding. It checks to see if Word is running. If it is, the code uses the existing instance of Word. If not, the code creates an instance of Word. First set a reference to Word (in the VB Editor, select Tools + References).
Ni pht hnh: www.giaiphapexcel.com

50

L p trnh VBA for Word


Sub ControlWordFromXL() Dim Dim Dim Dim Dim oWord As Word.Application WordWasNotRunning As Boolean oDoc As Word.Document myDialog As Word.Dialog UserButton As Long

'Get existing instance of Word if it's open; otherwise create a new one On Error Resume Next Set oWord = GetObject(, "Word.Application") If Err Then Set oWord = New Word.Application WordWasNotRunning = True End If On Error GoTo Err_Handler oWord.Visible = True oWord.Activate Set oDoc = oWord.Documents.Add oDoc.Range.Text = "Hi" Set myDialog = oWord.Dialogs(wdDialogEditReplace) With myDialog .Find = "Hi" .Replace = "Ho" End With On Error Resume Next UserButton = myDialog.Display() On Error GoTo Err_Handler MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox MsgBox "User Button = " & CStr(UserButton) "Find = " & CStr(myDialog.Find) "Replace = " & CStr(myDialog.Replace) "Direction = " & CStr(myDialog.Direction) "Wrap = " & CStr(myDialog.Wrap) "Format = " & CStr(myDialog.Format) "MatchCase = " & CStr(myDialog.MatchCase) "WholeWord = " & CStr(myDialog.WholeWord) "PatternMatch = " & CStr(myDialog.PatternMatch) "FindAllWordForms = " & CStr(myDialog.FindAllWordForms) "SoundsLike = " & CStr(myDialog.SoundsLike) "FindNext = " & CStr(myDialog.FindNext) "ReplaceOne = " & CStr(myDialog.ReplaceOne) "ReplaceAll = " & CStr(myDialog.ReplaceAll) "MatchByte = " & CStr(myDialog.MatchByte) "FuzzyFind = " & CStr(myDialog.FuzzyFind) "Destination = " & CStr(myDialog.Destination)

oDoc.Close savechanges:=wdDoNotSaveChanges Ni pht hnh: www.giaiphapexcel.com

51

L p trnh VBA for Word


If WordWasNotRunning Then oWord.Quit End If 'Make sure you release object references. Set oWord = Nothing Set oDoc = Nothing Set myDialog = Nothing 'quit Exit Sub Err_Handler: MsgBox "Word caused a problem. " & Err.Description, vbCritical, "Error: " _ & Err.Number If WordWasNotRunning Then oWord.Quit End If End Sub

3. Determine whether the insertion point is located at the end of a document


Article contributed by Bill Coan If you're already familiar with Start and End properties of objects in a Word document, feel free to skip to the end of this article for two helpful code snippets. Otherwise, read on to learn about Start and End properties of objects in a Word document.

3.1. Start Property


The Start property of every item in a Word document, whether a text character, an inline graphic, the anchor for a floating graphic, a table, or anything else, describes how many items are situated to the left of that item. For example, a text character at the start of a document is said to have a Start property of 0 (zero) because zero items are situated to the left of it. The second character in a document is said to have a Start property of 1 (one) because there is one item situated to the left of it.

3.2. End Property


The End property of every item in a Word document describes how many items are situated to the left of that item, including the item itself.
Ni pht hnh: www.giaiphapexcel.com

52

L p trnh VBA for Word For example, a text character at the start of a document is said to have an End property of 1 (one) because one item is situated to the left of it, counting the character itself. The second character in a document is said to have an End property of 2 (two) because there are two characters situated to the left of it, counting itself. A new, blank Word document contains a single empty paragraph, which is to say, a single character that happens to be a paragraph mark. Initially, the paragraph mark character has a Start property of 0, because zero items are situated to the left of it, and an End property of 1, because one item is situated to the left of it, counting itself. When an item in a Word document is selected, Word's Selection object takes on the Start and End properties of that Item. If multiple items are selected, the Selection object takes on the Start property of the first item and the End property of the last item. For example, if the entire contents of a Word document are selected, the Selection object takes on the Start property of the first character in the document and the End property of the final paragraph mark.

3.3. Are We There Yet?


The following code takes advantage of this to determine whether the selection includes the document's final paragraph mark.
' Determine whether selection includes final paragraph mark Dim SelectionIncludesFinalParagraphMark As Boolean If Selection.Type = wdSelectionNormal _ And Selection.End = ActiveDocument.Content.End _ Then SelectionIncludesFinalParagraphMark = True

When Word's Insertion Point is flashing in a document, the Selection object's Start and End properties are equal to each other and represent how many items are to the left of the Insertion Point. Since the Insertion Point can travel up to but never past the final paragraph mark in a document, the Start and End properties of the Selection object will never get higher than the Start value of the final paragraph mark, which is to say, one less than the End property of the final paragraph mark. The following code takes advantage of this to determine whether the insertion point is flashing in front of the document's final paragraph mark.
' Determine whether insertion point is flashing at end of document Dim InsertionPointFlashingAtEndOfDoc As Boolean If Selection.Type = wdSelectionIP And Selection.End = ActiveDocument.Content.End - 1 Then Ni pht hnh: www.giaiphapexcel.com

53

L p trnh VBA for Word


InsertionPointFlashingAtEndOfDoc = True

4. Detect whether a table cell is empty


Article contributed by Bill Coan

4.1. Method 1
Use the range object to detect empty cells based on the idea that an empty cell consists of a paragraph mark followed by Chr(7).
Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row For Each oRow In Selection.Tables(1).Rows For Each oCell In oRow.Cells If oCell.Range.Text = Chr(13) & Chr(7) Then MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Next oCell Next oRow End Sub

4.2. Method 2
Use a range variable, set it to mark each cell's range; move the end of the range so that the end of cell marker and paragraph marker are not included in the range; (to do this you only have to move it by one character); and get the text within the range. If the cell is empty there will be no text within the range
Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row Dim MyRange As Range For Each oRow In Selection.Tables(1).Rows For Each oCell In oRow.Cells Set MyRange = oCell.Range MyRange.End = MyRange.End - 1 If Len(MyRange.Text) = 0 Then MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Ni pht hnh: www.giaiphapexcel.com

54

L p trnh VBA for Word


Next oCell Next oRow End Sub

4.3. Method 3
Here's some code that is similar, but which selects each cell before announcing whether it is empty.
Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row Dim MyRange As Range For Each oRow In Selection.Tables(1).Rows For Each oCell In oRow.Cells If Selection.Text = Chr(13) & Chr(7) Then oCell.Select MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Next oCell Next oRow End Sub

5. Determine the page number at the current cursor position


Article contributed by Bill Coan Use:
Selection.Information (wdActiveEndPageNumber)

6. Determine the number of pages in a document


Article contributed by Bill Coan Solution #1:
Selection.Information(NumberOfPagesInDocument)

Solution #2:
Ni pht hnh: www.giaiphapexcel.com

55

L p trnh VBA for Word


ActiveDocument.BuiltInDocumentProperties("Number of Pages")

Solution #3:
ActiveDocument.Content.ComputeStatistics(wdStatisticPages)

The third method is the most reliable, but the slowest.

7. How to get the column number of the selection (in a document containing snaking, or newspaper-style, columns)
Article contributed by Cindy Meister Use:
With Dialogs(wdDialogFormatColumns) 'Gets the column number of the currently selected column Debug.Print .ColumnNo End With

Unfortunately this only works for selections, not for ranges.

8. Getting help with calling Word's built-in dialogs using VBA (and why doing so can be much more useful than you'd think)
Article contributed by Jonathan West and Dave Rado

8.1. Where to find Help on them


There are two Help topics in Word VBA Help that are required reading to get you started with builtin dialogs: Displaying built-in Word dialog boxes and Built-in dialog box argument lists. Unfortunately, in the latter article, Microsoft listed the arguments you can use but forgot to mention what the arguments mean or what values they can take! Fortunately, the dialog box arguments are almost identical to the arguments of the commands of WordBasic, so if you know one, you can work out the other. Therefore, the WordBasic Help file is at present by far the best resource for programmers wanting to use the dialogs. It is an absolute must-have. Even more fortunately, the WordBasic Help is now available on-line click here to download it.

8.2. Why use built-in dialogs?


There are four reasons to use them. 1. One obvious use is in order to display them to the user.
Ni pht hnh: www.giaiphapexcel.com

56

L p trnh VBA for Word 2. You can execute a built-in dialog without actually displaying it, thus allowing you to execute all of the relevant settings simultaneously; whereas with VBA, each statement executes one at a time. As a result, using the wdDialog method can sometimes make your code run much faster and use far less memory. A good example is wdDialogFormatBordersAndShading. Using this, you can execute all your borders and shading arguments with one Execute statement; whereas using native VBA, you have to set many separate properties one at a time (top border, bottom border etc.). As a result, not only will your code run much faster if you execute the built-in dialog, but you are far less likely to get Formatting too complex errors while your code is running. Consider the following, for instance:
With Dialogs(wdDialogFormatBordersAndShading) .ApplyTo = 3 .Shadow = 0 .Shading = 0 .Foreground = 0 .Background = 0 .LeftStyle = 0 .RightStyle = 0 .TopStyle = 0 .BottomStyle = 0 .HorizStyle = 0 .VertStyle = 0 .Execute End With

All the above properties are executed in one go by the .Execute statement, whereas if you use native VBA as shown below, each line executes before the next line starts to run, and so the macro runs far more slowly and uses far more memory:
With Selection.Tables(1) With .Shading .Texture = wdTextureNone .ForegroundPatternColor = wdColorAutomatic .BackgroundPatternColor = wdColorAutomatic End With .Borders(wdBorderLeft).LineStyle = wdLineStyleNone .Borders(wdBorderRight).LineStyle = wdLineStyleNone .Borders(wdBorderTop).LineStyle = wdLineStyleNone .Borders(wdBorderBottom).LineStyle = wdLineStyleNone .Borders(wdBorderHorizontal).LineStyle = wdLineStyleNone .Borders(wdBorderVertical).LineStyle = wdLineStyleNone .Borders(wdBorderDiagonalDown).LineStyle = wdLineStyleNone .Borders(wdBorderDiagonalUp).LineStyle = wdLineStyleNone .Borders.Shadow = False End With

3. A third reason for using the wdDialogs is that in a few cases, it does much of the work for you that you would otherwise have to write a lot of code to do for instance, if you want to change the page setup for the current selection, if you use wdDialogPageSetup, and set .ApplyPropsTo
Ni pht hnh: www.giaiphapexcel.com

57

L p trnh VBA for Word = 3, the section breaks are inserted for you. Doing the equivalent in native VBA is much more involved because you have to check whether there is already a section break before or after the selection, and whether the selection begins at the start of the doc and or ends at the end of the doc. Why write the code to do that in VBA when it's already been written in C++ by Microsoft? For instance, the following code snippet creates a landscape section at the selection (Chr$(34) is how you specify the inch (") symbol in WordBasic):
With Dialogs(wdDialogFilePageSetup) .ApplyPropsTo = 3 .PageWidth = 11.69 & Chr$(34) .PageHeight = 8.27 & Chr$(34) .TopMargin = -1.7 & Chr$(34) .BottomMargin = 0.81 & Chr$(34) .LeftMargin = iLeftMargin & Chr$(34) .RightMargin = 0.95 & Chr$(34) .Orientation = wdOrientLandscape .DifferentFirstPage = False .HeaderDistance = 0.28 & Chr$(34) .FirstPage = 0 .OtherPages = 0 .Execute End With

4. You can get and set information that would be difficult or, in some cases, impossible to get using native VBA, by looking (in code) at the settings of a built-in dialog without having to display the dialog.

9. Determine the position of the cursor on the page in points


(72 pts. = 1 inch = 2.54 cm) Article contributed by Bill Coan and Jonathan West Bearing in mind the caveat below, you can use:
x = Selection.Information(wdHorizontalPositionRelativeToPage) y = Selection.Information(wdVerticalPositionRelativeToPage)

9.1. Important caveat


y = Selection.Information(wdVerticalPositionRelativeToPage) does not return the correct value unless you are in Page view. In Normal view, it returns the distance from the top margin. Always set the view to be Page view, with Magnification set to 100%, before using the Information property to get the cursor position.

Ni pht hnh: www.giaiphapexcel.com

58

L p trnh VBA for Word

10. Determine whether the selection or range is at the start of a paragraph


Or of the current table, or section or of any object that has a Range property Article contributed by Bill Coan For paragraphs:
If Selection.Start = Selection.Paragraphs(1).Range.Start Then MsgBox "At start of a paragraph" Else MsgBox "Not at start of a paragraph" End If

For Ranges, substitute the range for the selection in the above code. For sections use Selection.Sections(1); for tables use Selection.Tables(1) etc.

11. Detect whether the first character in a selection is alphanumeric


Article contributed by Bill Coan
If Selection.Characters(1) Like "[a-zA-Z0-9]" Then MsgBox "Alphanumeric" If Selection.Characters(1) Like "[!a-zA-Z0-9]" Then MsgBox "Nonalphanumeric"

12. How to find out whether the current document is running in another application (such as Internet Explorer, Outlook, etc.)
Article contributed by Will Rickards and Dave Rado When you open a Word document in another application, such as Internet Explorer, and then subsequently open Word in the normal way, you unfortunately don't get two separate instances of Word, you get one. If you have written event procedures, or intercepted Word commands, then depending on the circumstances, they may still run when you are in a document that is running inside another application. However, many VBA functions don't work in that scenario, and some actually crash Word. So you may sometimes need to allow, in your code, for the possibility that the active document might be running inside another application.
Ni pht hnh: www.giaiphapexcel.com

59

L p trnh VBA for Word Two simple examples of VBA functions that won't work in this scenario are Application.Quit and ActiveDocument.Close, which both give the error This method or property is not available because the document is in another application. You can test for this scenario using the following code:
If Documents.Count > 1 Then If Len(ActiveWindow.Caption) = 0 Then 'The document is running within another application End If End If

Unfortunately, it seems that you can't find out which application it is running in, because:
MsgBox ActiveDocument.Application.Name

... returns Microsoft Word. Confusing, huh!

Ni pht hnh: www.giaiphapexcel.com

60

L p trnh VBA for Word

CHAPTER 3: Working with Bookmarks in VBA

1. Working with Bookmarks in VBA


Article contributed by Ibby

1.1. Types of Bookmarks


The most important thing you need to know when working with bookmarks in Word is that there are two types of bookmarks placeholder bookmarks and enclosing bookmarks. Before we proceed, and whenever you work with bookmarks, you should turn on display of bookmarks by going to Tools | Options | View and selecting Bookmarks. This makes it easier to see what's actually happening.

(1) Placeholder Bookmarks


If you click somewhere in the document and insert a bookmark it will look like a beam I this is a placeholder bookmark.

(2) Enclosing Bookmarks


Now, if you select some text and insert a bookmark it will look like the selected text is enclosed in square brackets ie: [selected text] this is an enclosing bookmark.

1.2. Inserting and retrieving text from a Bookmark


There are several methods of inserting text at/into a bookmark. The method you use depends on whether you need to retrieve the text from the bookmark at a later time. Lets look at the more obvious ways of inserting text at a bookmark.
ActiveDocument.Bookmarks("myBookmark").Range.Text = "Inserted Text"

If the bookmark is a placeholder bookmark, the inserted text will look like this: I Inserted Text If the bookmark is an enclosing bookmark, it will be deleted, and the inserted text will appear in it's place.
ActiveDocument.Bookmarks("myBookmark").Range.InsertBefore _ "Inserted Text" ActiveDocument.Bookmarks("myBookmark").Range.InsertAfter _ "Inserted Text"

Ni pht hnh: www.giaiphapexcel.com

61

L p trnh VBA for Word With both these methods, if the bookmark is a placeholder bookmark, the text will be inserted after the bookmark: I Inserted Text With enclosing bookmarks (even if the bookmark only encloses a space), the following occurs: InsertAfter [ Original Text ] Inserted Text InsertBefore [ Inserted Text Original Text ] In order to retrieve the text in a bookmark, the bookmark needs to be an enclosing bookmark. Then you can use the following to retrieve the text from the bookmark:
strBookmark = ActiveDocument.Bookmarks("myBookmark").Range.Text

You have already seen how to add text to an enclosing bookmark using the InsertBefore method above. But what if you want to insert text into a placeholder bookmark (making it an enclosing bookmark) so that you can retrieve the text from it at a later time ? And what if the bookmark is already an enclosing bookmark but you want to replace the text inside it ? There is no single command in VBA to achieve this. What you need to do is replace the bookmark with the inserted text (the bookmark is deleted), then re-create the bookmark around the inserted text. The following code is an example of how this is done:
Dim bmRange As Range Set bmRange = ActiveDocument.Bookmarks("myBookmark").Range bmRange.Text = "Inserted Text" ActiveDocument.Bookmarks.Add _ Name:="myBookmark", _ Range:=bmRange

See also Inserting text at a bookmark without deleting the bookmark

2. Inserting text at a bookmark without deleting the bookmark


Article contributed by Dave Rado

2.1. The problem


The following line:
ActiveDocument.Bookmarks("BookmarkName").Range.Text = "Hello world"

deletes the bookmark.

Ni pht hnh: www.giaiphapexcel.com

62

L p trnh VBA for Word Using the InsertAfter or InsertBefore method doesn't work satisfactorily either; if the bookmark is currently empty, then the line:
ActiveDocument.Bookmarks("Temp").Range.InsertAfter "Hello world"

will leave you with the bookmark at the start of the text you've just inserted, rather than containing it. And if the bookmark already contains some text, then Hello world will be appended to the existing text, instead of replacing it.

2.2. The solution


The best way to insert text at a bookmark without losing the bookmark which works reliably whether or not the bookmark currently encloses any text - is to set a range variable to the bookmark's range, as follows:
Dim BMRange As Range 'Identify current Bookmark range and insert text Set BMRange = ActiveDocument.Bookmarks("MyBookmark").Range BMRange.Text = "Hello world" 'Re-insert the bookmark ActiveDocument.Bookmarks.Add "MyBookmark", BMRange

If you have a macro which updates many bookmarks (for example, a userform macro), then repeating the above code over and over again would be very laborious, so the best plan is to call a subroutine with arguments as in the following example:
Sub UpdateBookmark(BookmarkToUpdate As String, TextToUse As String) Dim BMRange As Range Set BMRange = ActiveDocument.Bookmarks(BookmarkToUpdate).Range BMRange.Text = TextToUse ActiveDocument.Bookmarks.Add BookmarkToUpdate, BMRange End Sub

You could call it like this:


UpdateBookmark "NameOfBookMark", "String you want to insert"

3. How to create a menu to navigate to the non-hidden bookmarks in a document


Article contributed by Astrid Zeelenberg It's easy to navigate around the Headings in a document; just use Outline View. But what if you have set up some bookmarks of your own; for example, in an alphabetical listing, to mark where each letter of the alphabet begins? How can you make it easy for people to navigate around them?

Ni pht hnh: www.giaiphapexcel.com

63

L p trnh VBA for Word You can't use hyperlinks, because they wouldn't appear on every page (unless they were in the Header, but then they wouldn't be accessible). You could just ask people to select Insert + Bookmark + Goto, but that's not very user-friendly. A better idea would be to create a menu to do the job, and create an item on your menu for each bookmark, to go to that bookmark when selected. The following macros make this very simple just paste the following code into a module, run the first macro once, and from then on you can use the Bookmarks menu which it creates (click the Refresh list button on the Bookmarks menu whenever you subsequently add or remove bookmarks, and the menu will update).
Option Explicit Sub CreateBookMarkMenu() Dim Dim Dim Dim Dim oBookmark As Bookmark oBar As CommandBar oPopup As CommandBarPopup oButton As CommandBarButton ShowHiddenStatus As Boolean

'Find out whether hidden bookmarks set as "visible" or not, 'storing this setting in a variable so it can be returned to at the end. 'Then make the hidden bookmarks invisible '(we don't want cross-refs etc to appear in our menu) ShowHiddenStatus = ActiveDocument.Bookmarks.ShowHidden ActiveDocument.Bookmarks.ShowHidden = False CustomizationContext = ActiveDocument Set oBar = CommandBars.ActiveMenuBar 'First delete Bookmark menu if it already exists Set oPopup = CommandBars.FindControl(Tag:="Recreate") If Not oPopup Is Nothing Then oPopup.Delete End If

If ActiveDocument.Bookmarks.Count > 0 Then Set oPopup = oBar.Controls.Add(Type:=msoControlPopup, _ Before:=oBar.Controls.Count + 1) With oPopup .Caption = "Bookmarks" .Tag = "Recreate" End With For Each oBookmark In ActiveDocument.Bookmarks Set oButton = oPopup.Controls.Add(Type:=msoControlButton)

Ni pht hnh: www.giaiphapexcel.com

64

L p trnh VBA for Word


With oButton .Caption = oBookmark.Name .Style = msoButtonCaption .OnAction = "BookMarkSelect" End With Next 'Add a Refresh button at the bottom Set oButton = oPopup.Controls.Add(Type:=msoControlButton) With oButton .Caption = "Refresh list" .Style = msoButtonCaption .OnAction = "CreateBookMarkMenu" .BeginGroup = True End With End If ActiveDocument.Bookmarks.ShowHidden = ShowHiddenStatus Set Set Set Set oButton = Nothing oPopup = Nothing oBar = Nothing oBookmark = Nothing

End Sub Private Sub BookMarkSelect() If ActiveDocument.Bookmarks.Exists(CommandBars.ActionControl.Caption) Then ActiveDocument.Bookmarks(CommandBars.ActionControl.Caption). _ Range.Select End If End Sub Sub AutoOpen() 'Make sure the document's menu is visible when the document opens 'If the "customisation context" has been changed since it was last opened, 'the document-specific menus won't be visible! CustomizationContext = ActiveDocument End Sub

Ni pht hnh: www.giaiphapexcel.com

65

L p trnh VBA for Word

CHAPTER 4: Working with built-in dialogs

1. Calling FileOpen dialog in VBA does not allow opening of multiple files
Article contributed by Ibby If you call the FileOpen dialog using the following code, you will receive an error if you try to open multiple files using the dialog:
Dialogs(wdDialogFileOpen).Show

The workaround is to show the dialog by executing the appropriate toolbar button:
CommandBars.FindControl(ID:=23, Visible:=False).Execute

Setting Visible:=False (which is the default anyway) will ensure the button will be executed even if it's invisible (i.e.: even if the user has doesn't have it displayed in their toolbar). If you also want to find out which files were, in fact, opened by the user, you could use the following code which was originally posted in the newsgroups by Christa Siebe:
Sub LoadSeveralFiles() Dim Dim Dim Dim OpenDlg As CommandBarControl oldDocs() As String i As Long, j As Long IsNewDoc As Boolean

ReDim oldDocs(1 To Application.Documents.Count) 'because documents start with 1 as well... For j = 1 To Application.Documents.Count oldDocs(j) = Application.Documents(j).FullName Next Set OpenDlg = CommandBars.FindControl(ID:=23) OpenDlg.Execute If UBound(oldDocs) < Application.Documents.Count Then MsgBox "one or more files are opened!" For i = 1 To Application.Documents.Count IsNewDoc = True For j = 1 To UBound(oldDocs) If Application.Documents(i).FullName = oldDocs(j) Then IsNewDoc = False Exit For End If Next j Ni pht hnh: www.giaiphapexcel.com

66

L p trnh VBA for Word


If IsNewDoc Then MsgBox "Doc '" & Application.Documents(i).FullName & "' is a new one..." End If Next i End If End Sub

2. How to change the directory of the Save As dialog


Article contributed by Dave Rado and Jonathan West The easiest and best way is to specify the path you want in the Name argument of the Dialogs object:
With Dialogs(wdDialogFileSaveAs) .Name = "c:\windows\temp\" .Show End With

There are three advantages of using this method rather than methods such as ChangeFileOpenDirectory or Options.DefaultFilePath: It's simpler It means you don't have to change the path back again afterwards to what it was before. If the document has already been saved once, it will always default to its current path, unless you use the above method. Of course, you can use whichever folder you want in place of c:\windows\temp\. If you also want to preset the file name in the dialog with the current name of the active document, use the following code:
With Dialogs(wdDialogFileSaveAs) .Name = "C:\My Documents\" & ActiveDocument.Name .Show End With

Or (if path you want to set is the document's current path), you could just use:
With Dialogs(wdDialogFileSaveAs) .Name =ActiveDocument.FullName .Show End With

If you want to have the dialog preloaded with a different file name, then put your preferred filename there in place of ActiveDocument.Name, e.g.:
Ni pht hnh: www.giaiphapexcel.com

67

L p trnh VBA for Word


With Dialogs(wdDialogFileSaveAs) .Name = "C:\My Documents\temp.doc" .Show End With

3. How to set the default suggested filename to be displayed by the Save As dialog the first time a user saves a new document
Article originally contributed by Dave Rado with updates from Ibrahim Elnazak and Greg Chapman In a document that hasn't yet been saved, if you select File + Properties, type a Title, and click OK, the title will be displayed as the suggested file name in the Save As dialog the first time the document is saved.However, due to a VBA bug, using:
ActiveDocument.BuiltInDocumentProperties(wdPropertyTitle) = "My title"

doesn't work (it doesn't affect the default SaveAs filename); and the FileProperties dialog isn't directly accessible via VBA. However, the following does the trick (with thanks to Ibby who first came up with this workaround):
With Dialogs(wdDialogFileSummaryInfo) .Title = "My Title" .Execute End With

Important gotcha: Although this method does support long filenames, unfortunately it doesn't support delimiters such as underscore. If you set the title to My_Title, (as one does), the suggested filename will be My.doc. (You could get round this by intercepting the FileSave, FileSaveAll and FileSaveAs commands, and by writing an AutoClose macro to intercept the event of the user closing the document and being asked if they want to save changes, but that would be very kludgy indeed, whereas setting the Document Title is simplicity itself, provided you don't need it to support delimiters). In the meantime, here's a work-around to the work-around by Greg Chapman that ... well... works around it :-)
Sub ChangeProps() 'change properties If Documents.Count > 0 Then Set dlgProp = Dialogs(wdDialogFileSummaryInfo) ' Establish title, subject, author, and keywords values dlgProp.Title = MakeADocTitle("XXXX Form Options Specs.doc") dlgProp.Subject = strTitle dlgProp.Author = "Company Name" dlgProp.Keywords = strKeywords

Ni pht hnh: www.giaiphapexcel.com

68

L p trnh VBA for Word


' Set the values dlgProp.Execute ' Show the dialog for testing purposes dlgProp.Show End If End Sub Function MakeADocTitle(ByVal strTitle As String) As String arrSplit = Split(strTitle, " ") MakeADocTitle = arrSplit(0) For I = 1 To UBound(arrSplit) MakeADocTitle = MakeADocTitle & Chr(95) & arrSplit(I) Next I MakeADocTitle = MakeADocTitle End Function

4. Passwords not saved when calling FileSaveAs dialog from VBA


Article contributed by Ibby If you call the FileSaveAs dialog (Word 97) with the following code:
Dialogs(wdDialogFileSaveAs).Show

the passwords entered under Options Password to Open, Password to Modify do not stick. There are two workarounds, depending on whether you need to change the settings in the dialog via code: Either:
CommandBars.FindControl(ID:=748, Visible:=False).Execute

Or:
Dim strFileName As String With Dialogs(wdDialogFileSaveAs) If .Display = -1 Then strFileName = .Name .Update .Name = strFileName .Execute End If End With Ni pht hnh: www.giaiphapexcel.com

69

L p trnh VBA for Word

5. Force the user to save documents into a particular folder or a subfolder of that folder
Article contributed by Bill Coan Use a FileSave macro that will run in place of Word's own built-in File Save routine. The macro allows the user to save directly to a target folder or to a subfolder within that folder, but not to a location outside the target folder. To modify this macro for your own needs, change the path to whatever you want in the line:
.Name = "C:\My Documents\Test"

... and modify both the path and the number of characters in the Left$ test, in the line:
If Left$(CurDir, 20) <> "C:\My Documents\Test" Then Sub FileSave() Dim UserSaveDialog As Dialog Set UserSaveDialog = Dialogs(wdDialogFileSaveAs) 'save changes if doc has been saved previously If ActiveDocument.Path <> "" Then ActiveDocument.Save Exit Sub End If With UserSaveDialog .Name = "C:\My Documents\Test" If .Display Then 'if user doesn't click Cancel button, 'quit with message if user has switched out of target folder, 'but don't quit if user has made a 'subfolder within the target folder If LCase$(Left$(CurDir, 20)) <> "c:\my documents\test" Then MsgBox "Documents can't be saved in that folder. Please try again." Exit Sub End If 'save the document according to user preferences UserSaveDialog.Execute End If End With End Sub

Ni pht hnh: www.giaiphapexcel.com

70

L p trnh VBA for Word

6. How to get the full path from the SaveAs dialog


Article contributed by Dave Rado and Astrid Zeelenberg Use
Dim PathAndFileName As String With Dialogs(wdDialogFileSaveAs) If .Display Then 'User pressed ok PathAndFileName = WordBasic.FileNameInfo(.Name, 5) 'If the user typed the path and filename instead of browsing to it, 'the VBA function CurDir() won't reflect the path; 'hence the need to use the Wordbasic function FileNameInfo to get the path End If End With

7. Force the File New dialog to display in List view


Article contributed by Bill Coan Create a FileNew macro that will run in place of Word's own File New routine
Sub FileNew() SendKeys "%2" Dialogs(wdDialogFileNew).Show End Sub

Change the SendKeys statement to %1 for Large Icon view or to %3 for Detail view.

Ni pht hnh: www.giaiphapexcel.com

71

L p trnh VBA for Word

CHAPTER 5: Working with events 1. Running a macro automatically when Word starts or quits
Article contributed by Dave Rado
To run a macro when Word starts

In a Global template, if you write a macro called AutoExec, it will automatically run when you launch Word.
To run a macro when Word quits

In a Global template, if you write a macro called AutoExit, it will automatically run when you quit Word.

1.1. Global templates


A global template is either an add-in, (that is, a template stored in Word's Startup directory), or the Normal.dot template. It is global in the sense that its macros; toolbar; keyboard and menu customisations; and AutoText entries are available in any document, regardless of what template that document is attached to.

2. Running a macro automatically when a document is created, opened or closed


Article contributed by Dave Rado

2.1. Using Document events


Open your template, press Alt+F11, and in the Project window of the VBA environment, doubleclick on Microsoft Word Objects, then on ThisDocument. On the toolbar you'll see two list boxes. If you pull down the one on the left, and change it from (General) to Document, a procedure called Document_New() will be created. If you then pull down the list box on the right, you'll see three events to choose from: Close, New and Open. You can select Close or Open from the list to insert a Document_Close() or Document_Open() procedure (or you can forget about the list boxes and just type, once you know the syntax). A Document_New() procedure will run when a document based on that template is created; a Document_Open() procedure will run whenever a document based on that template is opened; and Document_Close() will run a document based on that template is closed. Note that these procedures cannot be made global they will only be fired when documents based on the template are created or opened or closed.

Ni pht hnh: www.giaiphapexcel.com

72

L p trnh VBA for Word

2.2. Using Auto macros


Another way of achieving the same objective is to create a Module (Insert + Module), and write a macro called AutoNew(), AutoOpen(), or AutoClose(). If stored in any template other than Normal.dot, these will behave in the same way as Document events; i.e. they will be fired when documents attached to the template are created, opened or closed. However, if stored in Normal.dot, they will act globally in other words, they will be fired when any document is created, opened, or closed. This is in contrast with a Document_Open procedure stored in Normal.dot, which will only execute when documents based on Normal.dot are opened. Unfortunately, AutoNew, AutoOpen and AutoClose macros stored in an Addin (a .dot file stored in Word's Startup directory) will not behave globally. In fact there is no point in storing AutoNew, AutoOpen or AutoClose macros in an Addin, because you would (or should) never base a document on an Addin.

2.3. Using Application Events


If you want a macro to be fired whenever any document is opened, regardless of which template the document is attached to, the simplest way, as discussed above is to write an AutoOpen macro and store it in Normal.dot. However there are problems associated with storing macros in Normal.dot, so if you want to avoid that route, the answer is to use Application Events. Application Events stored in global Addins do behave globally. And rather confusingly, some application events relate to documents. In Word 97, you can use the DocumentChange event of the Application object to simulate global Auto macros; and in Word 2000, you can use the DocumentOpen, NewDocument and DocumentBeforeClose events of the application object. Storing these in an Addin works just like storing Auto macros in Normal.dot (i.e. they're global).

3. Writing application event procedures


Or how to intercept events affecting any open document, such as the user changing focus from one document to another Article contributed by Dave Rado, with acknowledgements to Bill Coan You can respond to certain application-level events using Auto macros see: Running a macro automatically when Word starts or quits. However, to respond to other application events you need to create an application object declared WithEvents i.e. declared in such a way that it will respond to events.

3.1. How to set up code that will respond to application events


Ni pht hnh: www.giaiphapexcel.com

73

L p trnh VBA for Word If you open a template, press Alt+F11, and in the Project window of the VBA environment, doubleclick on Microsoft Word Objects, you'll see a built-in Class Module called ThisDocument. If you open this, you can use the list boxes on the toolbar to help you create document event procedures. So it would make sense for there to be another built-in class module called ThisApplication; but Microsoft, in their wisdom, felt this would be too user-friendly, so you have to create your own ... Help is very unhelpful on the topic there isn't even a definition in Help of what a Class is! but if you're interested, Bill Coan covered the topic at http://msdn.microsoft.com/library/default.asp?url=/library/enus/dnword2k2/html/odc_wdappevnt.asp. In essence, though, a Class is simply a container object which allows you to create properties, which are objects in their own right, and then create properties and methods for those objects. Note that the big picture view of what we're about to do is this: We want to respond to events. We need an application variable (declared with WithEvents) to receive the events. We need a class module to serve as a container for the application variable. So here's how to create an application event procedure: 1. Create an Addin; that is, a .dot file stored in Word's startup directory so that it is global. 2. Open the Addin, and in the VB Editor, select Insert + Class Module. Rename the Class Module from Class1 to ThisApplication. Note: The Class Module and all the variables in this article can be called whatever you like; but I have tried to make all the names as meaningful as possible. I like ThisApplication because it shows that it is functionally analogous to the built-in ThisDocument class module. But note that your ThisApplication object is simply a container. It doesn't actually represent an application. It represents all the properties of the Class Object, whatever we define those properties to be. 3. Insert the following code in the Class Module:
Option Explicit Public WithEvents oApp As Word.Application

This specifies that oApp is an object variable which will be used to respond to the events triggered by the ActiveX object Word.Application. By declaring it publicly, you have made oApp a property of the class object ThisApplication. At this point you are simply declaring what type of object oApp is (its type being a Word
Ni pht hnh: www.giaiphapexcel.com

74

L p trnh VBA for Word Application object), and declaring the fact it will respond to events. You are not actually assigning the oApp variable to the Word application yet that comes later. So the oApp object doesn't actually exist yet (and no area of memory has been set aside for it yet). The WithEvents keyword can only be used in Class Modules and can only refer to ActiveX i.e. OLE-compliant objects. 4. Now select Insert + Module. In the Module, insert the following code:
Option Explicit Dim oAppClass As New ThisApplication

Where ThisApplication is the name of your Class Module. The statement creates an object variable oAppClass which references (is a pointer to) the class object ThisApplication which you created earlier. Using the keyword New in the declaration creates a new instance of the class object that is, it loads an instance of the class into memory and makes the variable oAppClass point to the newly created instance. From now on, you can refer to this new instance of the class by using the oAppClass variable. Now add the following code in the same Module:
Public Sub AutoExec() Set oAppClass.oApp = Word.Application End Sub

The oApp object is a property of the class object oAppClass, because you declared it as a public variable in the Class Module in step 3. The above code creates an instance of the oApp object (loads it into memory), and makes it actually refer to the Word.Application object. In effect it makes the oApp object exist. To put it another way, you have now assigned the actual Active-X Word Application to the variable, whereas previously you had only declared what type of variable it was going to be. By calling your procedure AutoExec, you make it run automatically when your Addin loads and your Addin will automatically load when Word loads. 5. Now run the AutoExec macro, by clicking in it and pressing F5, to initialise the oApp object. 6. Switch to your ThisApplication Class Module. You'll see two list boxes on the toolbar. If you pull down the one on the left, and change it from (General) to oApp, a procedure called oApp_Quit() will be created. If you then pull down the list box on the right, you'll see several other application events to choose from (and you can delete the oApp_Quit() procedure later, if you don't want it): a) In Word 97, you only have the choice of Quit and DocumentChange.
Ni pht hnh: www.giaiphapexcel.com

75

L p trnh VBA for Word b) In Word 2000, you have the choice of: Quit DocumentChange DocumentBeforeClose DocumentBeforePrint DocumentBeforeSave DocumentOpen NewDocument WindowActivate WindowBeforeDoubleClick WindowBeforeRightClick WindowDeactivate WindowSelectionChange Note that (rather confusingly) these are all Application events (even though some of them refer to documents doing things) as distinct from Document events such as Document_Close. Because it is an Application event, for instance, a procedure called oApp_DocumentBeforeClose will fire when any document closes; whereas because it is a Document event, a Document_Close procedure will only fire when documents based on the template that the Document_Close procedure is stored in are closed. 7. Having inserted the event procedure you want using the listboxes, add your code, and you're done. Note: If you want to monitor application events affecting documents based on a specific template, you can either store the event code in a global Addin as described above and test for which template is in use by using ActiveDocument.AttachedTemplate; or you can store the event code in the ThisDocument Class Module of the relevant template.

3.2. Summary of set-up procedure

Ni pht hnh: www.giaiphapexcel.com

76

L p trnh VBA for Word

3.3. oApp_Quit
An oApp_Quit event procedure works in exactly the same way as an AutoExit macro it fires when Word quits. Unfortunately, both the AutoExit macro and the oApp_Quit procedure fire after any Do you want to save changes dialogs have appeared. There is no event of the Application object analogous to an AutoExec macro, however.

3.4. oApp_DocumentChange
The DocumentChange event triggers whenever you open/close/create a document and whenever you change focus from one document to another. If the event code is stored in an Addin, it will be global. In Word 2000, there are separate events you can use for each of these oApp_NewDocument for when a new document is created, etc. But in Word 97, you can use oApp_DocumentChange to simulate them. Even in Word 2000, simulating them sometimes works better than using the proper events. .

Ni pht hnh: www.giaiphapexcel.com

77

L p trnh VBA for Word

3.5. oApp_DocumentBeforeClose, oApp_DocumentOpen, oApp_NewDocument, oApp_ WindowActivate, oApp_WindowDeactivate


All of these combined at least in theory replace (and build on) the functionality of oApp_DocumentChange. The oApp_DocumentBeforeClose, oApp_DocumentOpen, oApp_NewDocument event procedures are covered in the article: How to create global event procedures similar to AutoOpen, AutoNew and AutoClose, without using Normal.dot. The oApp_ WindowActivate, oApp_WindowDeactivate events are self-explanatory. It's safest to use the list boxes on the toolbar to insert the event procedures, as the syntax is complicated.

3.6. oApp_DocumentBeforePrint, oApp_DocumentBeforeSave


These are discussed in the article: Intercepting events like Save and Print.

3.7. oApp_WindowBeforeDoubleClick, oApp_WindowBeforeRightClick, oApp_WindowSelectionChange


Again, these are self-explanatory, and it's safest to use the list boxes on the toolbar to insert the event procedures, to ensure that you get the syntax right. Using the WindowBeforeDoubleClick and WindowBeforeRightClick events seem to have a significant performance hit on Word; but the WindowSelectionChange events seems to work well (subject, that is, to the code you put in it).

4. How to create global event procedures similar to AutoOpen, AutoNew and AutoClose, without using Normal.dot
Article contributed by Ibby and Dave Rado

1. Using Word 97
If you create a DocumentChange event and store it in an Addin, it will be triggered when any document is created, opened, closed or the focus changes from one document to another. You can take advantage of this to simulate AutoNew, AutoOpen, AutoClose macros (as well as being able to monitor when the focus changes). Unlike the application event DocumentChange, AutoNew, AutoOpen and AutoClose macros only behave globally if stored in Normal.dot. You can do it as follows: In a Module:
Ni pht hnh: www.giaiphapexcel.com

78

L p trnh VBA for Word


Option Explicit Dim oAppClass As New ThisApplication Public oldNoOfOpenDocs As Long Public FirstNewDoc As Boolean Public Sub AutoExec() Set oAppClass.oApp = Word.Application oldNoOfOpenDocs = 0 FirstNewDoc = True End Sub

In a Class Module (name this ThisApplication):


Option Explicit Public WithEvents oApp As Word.Application

Private Sub oApp_DocumentChange() On Error GoTo ExitCode Dim newNoOfOpenDocs As Long Dim docAdded As Boolean Dim docClosed As Boolean newNoOfOpenDocs = Application.Documents.Count If newNoOfOpenDocs > oldNoOfOpenDocs Then docAdded = True If ActiveDocument.Name = "Document1" And FirstNewDoc Then FirstNewDoc = True Else FirstNewDoc = False End If oldNoOfOpenDocs = oldNoOfOpenDocs + 1 ElseIf oldNoOfOpenDocs > newNoOfOpenDocs Then docClosed = True FirstNewDoc = False oldNoOfOpenDocs = oldNoOfOpenDocs - 1 End If If docAdded Then If Len(ActiveDocument.Path) = 0 Then Call PsuedoAutoNew Else Call PsuedoAutoOpen End If Ni pht hnh: www.giaiphapexcel.com

79

L p trnh VBA for Word


ElseIf docClosed Then Call PsuedoAutoClose ElseIf FirstNewDoc Then If Len(ActiveDocument.Path) = 0 Then Call PsuedoAutoNew Else Call PsuedoAutoOpen End If Else Call DocChangedFocus End If Exit Sub ExitCode: End Sub

Private Sub PsuedoAutoNew() 'Your code here End Sub

Private Sub PsuedoAutoOpen() 'Your code here End Sub

Private Sub PsuedoAutoClose() 'Your code here End Sub

Private Sub DocChangedFocus() 'Your code here End Sub

Notes: 1. Unfortunately, unlike a true AutoClose macro or DocumentBeforeClose event, the PsuedoAutoClose fires after the document has closed rather than before; so in this respect, this workaround is no substitute for the real thing; but maybe it's better than no workaround at all. (This is logical, if unfortunate, because the document focus hasn't changed until the
Ni pht hnh: www.giaiphapexcel.com

80

L p trnh VBA for Word document being closed has completely closed.). Also, (and unfortunately), if you quit Word with only one document open, the PsuedoAutoClose macro doesn't fire at all. And if you quit Word using File + Exit, even the (Word 2000-specific) DocumentBeforeClose event, which is covered below, fires after the message asking whether you want to save changes has appeared!! This bug is unfortunate to say the least, as it means that if you want to perform some validation before that dialog appears, you have to use an AutoClose macro (see the article: A pseudo DocumentBeforeClose Event). 2. In one important respect, the PsuedoAutoNew and PsuedoAutoOpen macros work better than the Word 2000-specific NewDocument and DocumentOpen events. When you start Word, a new blank document is created; the PsuedoAutoNew macro runs, but neither AutoNew nor the (Word 2000-specific) NewDocument events are triggered. Similarly, if you start Word with an AutoExec macro that opens a document, or that allows the user to open a document, for example:
Public Sub AutoExec() Dim myFile As String myFile = Application.RecentFiles(1).Path & "\" _ & Application.RecentFiles(1).Name Documents.Open myFile End Sub

... AutoOpen and the PsuedoAutoOpen macro both fire, but the (Word 2000-specific) DocumentOpen event is not triggered. So even Word 2000 developers who don't have to cater for Word 97 users may still prefer to use the use the DocumentChange event to simulate AutoOpen and AutoNew, as it's more reliable.

2. Using Word 2000 and later versions


In Word 2000 and above, you can store NewDocument, DocumentOpen and DocumentBeforeClose event procedures in an Addin, and they will be global. They would look like this:
Private Sub oApp_NewDocument(ByVal Doc As Document) 'Your code here End Sub Private Sub oApp_DocumentOpen(ByVal Doc As Document) 'Your code here End Sub Private Sub oApp_DocumentBeforeClose(ByVal Doc As Document, _ Ni pht hnh: www.giaiphapexcel.com

81

L p trnh VBA for Word


Cancel As Boolean) 'Your code here End Sub

In the case of the DocumentBeforeClose event, the Cancel parameter allows you to prevent the document closing if required, e.g.:
Private Sub oApp_DocumentBeforeClose(ByVal Doc As Document, Cancel As Boolean) If ActiveDocument.Paragraphs.Count > 3 Then Cancel = True MsgBox "You are not allowed more than 3 paragraphs." End If End Sub

But see the Notes at the end of the Word 97 section for a discussion of the numerous gotchas associated with this and with all three of these events and for some workarounds.

5. Intercepting events like Save and Print


Article contributed by Dave Rado, Anna-Karin Bohman and Jonathan West

5.1. Intercepting commands


To intercept any Word command, you can: 1. 2. Press Alt+ F8 to bring up the Macros dialog and where it says Macros in, select Word Commands. Find and select one of the commands you want to intercept for instance, to intercept the Print commands you need to find FilePrint and FilePrintDefault. To intercept the Save commands you need to find FileSave, FileSaveAs and FileSaveAll Where it says Macros in, select the template you want to store the macro in, and click Create. The code needed to execute the command will be written for you; just add your own code.

3. 4.

In the case of the Save event, writing FileSave, FileSaveAs and FileSaveAll macros isn't enough, because they won't intercept the user closing an unsaved document and being asked if they want to save changes but you can intercept that by writing a macro called AutoClose; or by writing a Document_Close event procedure in the ThisDocument code module.

See also: Word commands, and their descriptions, default shortcuts and menu assignments
Ni pht hnh: www.giaiphapexcel.com

82

L p trnh VBA for Word How to make the Paste Special dialog default to pasting Inline rather than Floating

If you are using a version of Word other than English If you are not using an English version of Word, and if you create a macro using the name shown in the list of Word commands, only the description of what the macro does will be added to the new macro, not the necessary code. To get the necessary code, you have to create a macro using the English name for the command! But how do you find out the English name? You can get a full list of the English commands from here. When you get the command's name right, the listbox at the very bottom will display the description of what the command does:

5.2. Intercepting events (Word 2000 or later)


Intercepting a command isn't quite the same as intercepting events, but in most cases it's the best you can do. However, in Word 2000 or later, a number of new Application Events were made available in VBA. Two Application Events you can use include DocumentBeforeSave and DocumentBeforePrint. Both of these, but especially the former, work better than trying to intercept the relevant commands. If not familiar with writing application event procedures, see the article: Writing application event procedures. A DocumentBeforePrint event procedure looks like this:
Private Sub oApp_DocumentBeforePrint(ByVal Doc As Document, _ Cancel As Boolean) Ni pht hnh: www.giaiphapexcel.com

83

L p trnh VBA for Word


'Your code here End Sub

If you want to prevent printing from occurring in certain circumstances, you can set the Cancel variable to True, e.g.:
Private Sub oApp_DocumentBeforePrint(ByVal Doc As Document, _ Cancel As Boolean) Dim Result As Long Result = MsgBox("Have you checked the " & "printer for letterhead paper?", vbYesNo) If Result = vbNo Then Cancel = True End Sub

A DocumentBeforeSave procedure looks like this:


Private Sub oApp_DocumentBeforeSave(ByVal Doc As Document, _ SaveAsUI As Boolean, Cancel As Boolean) 'Your code here End Sub

Again, you can set Cancel = True if you want to cancel the save. If you set the SaveAsUI variable to True, the Save As dialog box will be displayed.

6. A Pseudo DocumentBeforeClose Event


Or: How to perform validation on a document when the user tries to close it, before the user is asked to save changes Article contributed by Ibby There is no DocumentBeforeClose event in Word97. Even in Word 2000, there are serious problems with using the DocumentBeforeClose application event, in that if the user quits Word using File + Exit, the DocumentBeforeClose event fires after the messages asking whether you want to save changes, instead of before!! This bug makes the DocumentBeforeClose event useless if you want to validate anything before the save changes message appears. However, you can create a fake DocumentBeforeClose event, in either version of Word which does always fire before the the save changes message appears as follows: The code needs to be in the Document_Close event procedure (ThisDocument module) or in a macro named AutoClose in a normal code module so that it runs when the user tries to close the document. The code needs to do the following: Mark the document as unsaved this causes a prompt from Word, asking whether you want to save the changes or cancel the close operation.
Ni pht hnh: www.giaiphapexcel.com

84

L p trnh VBA for Word Dismiss the dialog before the user sees it, using the SendKeys statement. So, the following code will prevent the user from being able to close the document:
Public Sub AutoClose() ' Mark the document as unsaved so a prompt appears asking whther to save changes. ActiveDocument.Saved = False ' Dismiss the displayed prompt. SendKeys "{ESC}" End Sub

To use this in a validation routine, just wrap this in the validation code. The following example prevents closing of the document if it contains more than three paragraphs:
Public Sub AutoClose() If ActiveDocument.Paragraphs.Count > 3 Then ' If there are more than 3 paragraphs in document, ' display a msg to user. MsgBox "You are not allowed more than 3 paragraphs." ' Then cancel closing of the document. ' Mark the document as unsaved so a prompt ' appears asking whether to save changes. ActiveDocument.Saved = False ' Dismiss the displayed prompt. SendKeys "{ESC}" End If End Sub

7. How can I prevent Word from running macros automatically when I create a new instance of Word, open a Word document or create a new one?
Article contributed by Jonathan West If you are running a macro that opens (or creates) several files, the last thing you may want is for an AutoOpen (or AutoNew) macro to fire up each time. For this you can use:

Ni pht hnh: www.giaiphapexcel.com

85

L p trnh VBA for Word


WordBasic.DisableAutoMacros 1 WordBasic.DisableAutoMacros 0 'Disables auto macros 'Enables auto macros

This command is also very useful when launching an instance of Word from another application, or from VB, when you will generally not want any AutoExec macros to fire.

8. Assigning a macro to the tab key


Article contributed by Bill Coan Since the TAB key isn't accepted as a legitimate choice in the Customize dialog, use a macro to make the assignment.
Notes

The following macro assigns the tab key to a macro called MacroNameGoesHere. Simply replace MacroNameGoesHere with the name of the macro that you want to run and you'll be all set, except for the limitations noted afterward. Code This code is provided for illustrative purposes only and is not warranted to be suitable for any particular business purpose. The code may be freely copied for any lawful business purpose.
Sub AssignTabKey() KeyBindings.Add KeyCode:=BuildKeyCode(wdKeyTab), KeyCategory:= _ wdKeyCategoryMacro, Command:="MacroNameGoesHere" End Sub

Limitation #1 When the cursor is situated in a table cell, the tab key isn't interpreted as a tab key. Instead, it is interpreted as a NextCell command. If you want to control the action of the tab key in this situation, you must create a subroutine called NextCell. For example:
Sub NextCell() 'Code listed here will control tab key inside a table cell End Sub

Limitation #2 When the cursor is situated in a form field in a protected section of a protected document, the tab key isn't interpreted as a tab key. Instead, it executes a built-in routine that lies beyond vba's reach.

9. Change the behavior of the TAB key inside a table cell


Article contributed by Bill Coan

Ni pht hnh: www.giaiphapexcel.com

86

L p trnh VBA for Word Note When the cursor is inside an unprotected table and you press the Tab key, Word runs a built-in routine called NextCell. If you create a custom version of this routine, Word will run your version instead of the built-in version. If the table is in a section of a document protected for forms, Word runs a built-in routine that lies beyond the reach of Visual Basic for Applications. Solution Use a macro to move the cursor downward through each column when the tab key is pressed (or upward when Shift+Tab is pressed). Design the macro so that, when the cursor reaches the bottom of a column, it moves to the top of the next column. Caveats The following routines have been tested only on tables where all rows have the same number of columns and all columns have the same number of rows. Using them disables the ability to create new rows by tabbing. 1. 2. Click Macro on the Tools menu, then click Macros... on the submenu. Under Macro Name, enter NextCell, then click Create Word will show the following code, which represents the built-in NextCell routine:
Sub NextCell() ' ' NextCell Macro ' Moves to the next table cell ' Selection.MoveRight Unit:=wdCell End Sub

3.

Replace the built-in code with the following code:


Sub NextCell() Dim NeedToSelect As Long, CurrentRow As Long, CurrentColumn As Long Do While Selection.Information(wdStartOfRangeColumnNumber) > _ Selection.Information(wdMaximumNumberOfColumns) Selection.MoveLeft NeedToSelect = 1 Loop If NeedToSelect = 1 Then Selection.Cells(1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Exit Sub End If CurrentRow = Selection.Information(wdStartOfRangeRowNumber) CurrentColumn = Selection.Information(wdStartOfRangeColumnNumber)

Ni pht hnh: www.giaiphapexcel.com

87

L p trnh VBA for Word


If CurrentRow < Selection.Information(wdMaximumNumberOfRows) Then Selection.Tables(1).Cell(CurrentRow + 1, CurrentColumn).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 ElseIf CurrentColumn < Selection.Information(wdMaximumNumberOfColumns) Then Selection.Tables(1).Cell(1, CurrentColumn + 1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Else Selection.Tables(1).Cell(1, 1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 End If System.Cursor = wdCursorNormal End Sub

4.

Similarly create a PrevCell macro as with the following code, to intercept pressing Shift+Tab:
Sub PrevCell() Dim NeedToSelect As Long, CurrentRow As Long, CurrentColumn As Long, _ NumRows As Long, NumCols As Long Do While Selection.Information(wdStartOfRangeColumnNumber) > _ Selection.Information(wdMaximumNumberOfColumns) Selection.MoveLeft NeedToSelect = 1 Loop If NeedToSelect = 1 Then Selection.Cells(1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Exit Sub End If NumRows = Selection.Information(wdMaximumNumberOfRows) NumCols = Selection.Information(wdMaximumNumberOfColumns) CurrentRow = Selection.Information(wdStartOfRangeRowNumber) CurrentColumn = Selection.Information(wdStartOfRangeColumnNumber) If CurrentRow > 1 Then Selection.Tables(1).Cell(CurrentRow - 1, CurrentColumn).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 ElseIf CurrentColumn > 1 Then Selection.Tables(1).Cell(NumRows, CurrentColumn - 1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Else Selection.Tables(1).Cell(NumRows, NumCols).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 End If

Ni pht hnh: www.giaiphapexcel.com

88

L p trnh VBA for Word


System.Cursor = wdCursorNormal End Sub

5. 6. 7. 8. 9.

Select Save Normal on the File menu. Select Close and Return to MS Word on the File menu. Position the cursor in a table and press the TAB key If not satisfied with the results, modify the code as desired. To return the TAB key to its normal functionality, delete or rename the macros.

10. Prevent a file from showing up on the recently used files list
Article contributed by Bill Coan Turn off the list when the document opens and turn it back on again when the document closes. In order to do this, you need to create some document variables to store the user's original settings. Then you need to capture those settings and store them in the document variables before turning off the list. Finally, you need to use the document variables to turn the list back on with the user's original settings. A separate subroutine is used for each of these actions. Generally speaking, you will want to call these routines from the Document_Open and Document_Close events.
Sub CreateDocumentVariables() ActiveDocument.Variables.Add Name:="DisplayRecentFiles", Value:="0" ActiveDocument.Variables.Add Name:="RecentFilesMaximum", Value:="0" End Sub Sub RecordUserOptions() ''make a record of user settings so they can be restored after my ''last document is closed ActiveDocument.Variables("DisplayRecentFiles") = Application.DisplayRecentFiles ActiveDocument.Variables("RecentFilesMaximum") = Application.RecentFiles.Maximum End Sub Sub SetTemporaryOptions() 'Change user options to suit my requirements Application.DisplayRecentFiles = True Application.RecentFiles.Maximum = 4 End Sub Sub RestoreUserOptions() 'restore user settings to what they were before my first doc was opened Application.DisplayRecentFiles = Ni pht hnh: www.giaiphapexcel.com

89

L p trnh VBA for Word


ActiveDocument.Variables("DisplayRecentFiles") Application.RecentFiles.Maximum = ActiveDocument.Variables("RecentFilesMaximum") End Sub

11. How can I prevent users from editing the header of a document in Word 2000 or higher?
Article contributed by Bill Coan The following code, pasted into the This Document module of a Word 2000 Template, will keep users out of the header and footer of documents based on that template.
Option Explicit 'reserve memory for an application variable Private WithEvents wdApp As Word.Application Private Sub Document_New() 'assign Word to the application variable If wdApp Is Nothing Then Set wdApp = ThisDocument.Application End If End Sub Private Sub Document_Open() 'assign Word to the application variable If wdApp Is Nothing Then Set wdApp = ThisDocument.Application End If End Sub Private Sub wdApp_WindowSelectionChange(ByVal Sel As Selection) 'quit if active doc isn't attached to this template If ActiveDocument.AttachedTemplate <> ThisDocument Then Exit Sub 'get out of the header/footer if we're in it Select Case Sel.StoryType Case wdEvenPagesFooterStory, wdEvenPagesHeaderStory, _ wdFirstPageFooterStory, wdFirstPageHeaderStory, _ wdPrimaryFooterStory, wdPrimaryHeaderStory ActiveWindow.ActivePane.View.SeekView = wdSeekMainDocument Exit Sub Case Else End Select End Sub

Ni pht hnh: www.giaiphapexcel.com

90

L p trnh VBA for Word

CHAPTER 6: Working with properties

1. Using VBA, how can I get access to the Document Properties of a Word file without opening the document?
Article contributed by Jonathan West Microsoft makes an ActiveX DLL available called dsofile.dll, which allows you to read & write the document properties of an Office file without opening the file in a document editing window. To obtain it, go to Q224351 Note: Microsoft has updated DSOFile.dll to version 2, compatible with .NET. The template referenced in this article has been updated to use the later version of DOSFile.dll. The download includes the DLL itself, plus the source code for a small VB5/VB6 project that demonstrates its use. While the VB code can't be used unmodified in VBA, looking at it should give you a pretty good idea of how to use the DLL. In addition, you can download a template called ListProps.dot (by clicking on the link) that will use dsofile to list all the Office documents in a folder, including whichever of the built-in document properties you want to have. To run it, proceed as follows. 1. 2. 3. 4. Download and register dsofile.dll using regsvr32. Copy the ListProps template into your Word startup folder. Start Word. A new entry will appear in the Tools menu. Select the entry. In the dialog that appears, select the properties you want to include in your list, select the folder you want to have listed, and select the template you want to use as the document type for the list.

ListProps then uses dsofile to open each file in turn, get the properties, and puts the list into a table. Works with available properties of Word, Excel & PowerPoint files.

2. How to use a single VBA procedure to read or write both custom and built-in Document Properties
Article contributed by Astrid Zeelenberg When you work with Document Properties in code, most people end up with two functions or subroutines, one to write built-in Document Properties and one for custom Document Properties; because in each case the object used to refer to the Document Properties is different you have to
Ni pht hnh: www.giaiphapexcel.com

91

L p trnh VBA for Word use the CustomDocumentProperties and BuiltinDocumentProperties collection as appropriate. But this can be very inconvenient.

2.1. Writing Document Properties


However, you can write a procedure which checks whether the property you want to write the value for is custom or built-in, and then uses the appropriate collection. (Note: If you are not familiar with calling subroutines with arguments, see: How to cut out repetition and write much less code, by using subroutines and functions that take arguments). This is how to do it:
Public Sub WriteProp(sPropName As String, sValue As String, _ Optional lType As Long = msoPropertyTypeString) 'In the above declaration, "Optional lType As Long = msoPropertyTypeString" means 'that if the Document Property's Type is Text, we don't need to include the lType argument 'when we call the procedure; but if it's any other Prpperty Type (e.g. date) then we do Dim bCustom As Boolean On Error GoTo ErrHandlerWriteProp 'Try to write the value sValue to the custom documentproperties 'If the customdocumentproperty does not exists, an error will occur 'and the code in the errorhandler will run ActiveDocument.BuiltInDocumentProperties(sPropName).Value = sValue 'Quit this routine Exit Sub Proceed: 'We know now that the property is not a builtin documentproperty, 'but a custom documentproperty, so bCustom = True bCustom = True Custom: 'Try to set the value for the customproperty sPropName to sValue 'An error will occur if the documentproperty doesn't exist yet 'and the code in the errorhandler will take over ActiveDocument.CustomDocumentProperties(sPropName).Value = sValue Exit Sub AddProp: 'We came here from the errorhandler, so know we know that 'property sPropName is not a built-in property and that there's 'no custom property with this name 'Add it On Error Resume Next ActiveDocument.CustomDocumentProperties.Add Name:=sPropName, _ Ni pht hnh: www.giaiphapexcel.com

92

L p trnh VBA for Word


LinkToContent:=False, Type:=lType, Value:=sValue If Err Then 'If we still get an error, the value isn't valid for the Property Type 'e,g an invalid date was used Debug.Print "The Property " & Chr(34) & _ sPropName & Chr(34) & " couldn't be written, because " & _ Chr(34) & sValue & Chr(34) & _ " is not a valid value for the property type" End If Exit Sub ErrHandlerWriteProp: Select Case Err Case Else 'Clear the error Err.Clear 'bCustom is a boolean variable, if the code jumps to this 'errorhandler for the first time, the value for bCustom is False If Not bCustom Then 'Continue with the code after the label Proceed Resume Proceed Else 'The errorhandler was executed before because the value for 'the variable bCustom is True, therefor we know that the 'customdocumentproperty did not exist yet, jump to AddProp, 'where the property will be made Resume AddProp End If End Select End Sub

We could call the above procedure like this:


Sub Test() 'Author is a built-in property Call WriteProp(sPropName:="Author", sValue:="William Shakespeare") 'Date Updated is a custom document property Call WriteProp(sPropName:="Date Updated", sValue:="11 Mar 2001", _ lType:=msoPropertyTypeDate) End Sub

2.2. Reading Document Properties


The same principle can be used when reading Document Properties:
Ni pht hnh: www.giaiphapexcel.com

93

L p trnh VBA for Word


Function ReadProp(sPropName As String) As Variant Dim bCustom As Boolean Dim sValue As String On Error GoTo ErrHandlerReadProp 'Try the built-in properties first 'An error will occur if the property doesn't exist sValue = ActiveDocument.BuiltInDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ContinueCustom: bCustom = True Custom: sValue = ActiveDocument.CustomDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ErrHandlerReadProp: Err.Clear 'The boolean bCustom has the value False, if this is the first 'time that the errorhandler is runned If Not bCustom Then 'Continue to see if the property is a custom documentproperty Resume ContinueCustom Else 'The property wasn't found, return an empty string ReadProp = "" Exit Function End If End Function

We could call the function like this:


Sub Test() Dim PropVal As String PropVal = ReadProp("Author") Debug.Print PropVal PropVal = ReadProp("Date Completed") Debug.Print PropVal End Sub

Ni pht hnh: www.giaiphapexcel.com

94

L p trnh VBA for Word

3. Using VBA, how can I get access to the Document Properties of a Word file without opening the document?
Article contributed by Jonathan West Microsoft makes an ActiveX DLL available called dsofile.dll, which allows you to read & write the document properties of an Office file without opening the file in a document editing window. To obtain it, go to Q224351 Note: Microsoft has updated DSOFile.dll to version 2, compatible with .NET. The template referenced in this article has been updated to use the later version of DOSFile.dll. The download includes the DLL itself, plus the source code for a small VB5/VB6 project that demonstrates its use. While the VB code can't be used unmodified in VBA, looking at it should give you a pretty good idea of how to use the DLL. In addition, you can download a template called ListProps.dot (by clicking on the link) that will use dsofile to list all the Office documents in a folder, including whichever of the built-in document properties you want to have. To run it, proceed as follows. 1. 2. 3. 4. Download and register dsofile.dll using regsvr32. Copy the ListProps template into your Word startup folder. Start Word. A new entry will appear in the Tools menu. Select the entry. In the dialog that appears, select the properties you want to include in your list, select the folder you want to have listed, and select the template you want to use as the document type for the list.

ListProps then uses dsofile to open each file in turn, get the properties, and puts the list into a table. Works with available properties of Word, Excel & PowerPoint files.

4. How to use a single VBA procedure to read or write both custom and built-in Document Properties
Article contributed by Astrid Zeelenberg When you work with Document Properties in code, most people end up with two functions or subroutines, one to write built-in Document Properties and one for custom Document Properties; because in each case the object used to refer to the Document Properties is different you have to use the CustomDocumentProperties and BuiltinDocumentProperties collection as appropriate. But this can be very inconvenient.

4.1. Writing Document Properties


Ni pht hnh: www.giaiphapexcel.com

95

L p trnh VBA for Word However, you can write a procedure which checks whether the property you want to write the value for is custom or built-in, and then uses the appropriate collection. (Note: If you are not familiar with calling subroutines with arguments, see: How to cut out repetition and write much less code, by using subroutines and functions that take arguments). This is how to do it:
Public Sub WriteProp(sPropName As String, sValue As String, _ Optional lType As Long = msoPropertyTypeString) 'In the above declaration, "Optional lType As Long = msoPropertyTypeString" means 'that if the Document Property's Type is Text, we don't need to include the lType argument 'when we call the procedure; but if it's any other Prpperty Type (e.g. date) then we do Dim bCustom As Boolean On Error GoTo ErrHandlerWriteProp 'Try to write the value sValue to the custom documentproperties 'If the customdocumentproperty does not exists, an error will occur 'and the code in the errorhandler will run ActiveDocument.BuiltInDocumentProperties(sPropName).Value = sValue 'Quit this routine Exit Sub Proceed: 'We know now that the property is not a builtin documentproperty, 'but a custom documentproperty, so bCustom = True bCustom = True Custom: 'Try to set the value for the customproperty sPropName to sValue 'An error will occur if the documentproperty doesn't exist yet 'and the code in the errorhandler will take over ActiveDocument.CustomDocumentProperties(sPropName).Value = sValue Exit Sub AddProp: 'We came here from the errorhandler, so know we know that 'property sPropName is not a built-in property and that there's 'no custom property with this name 'Add it On Error Resume Next ActiveDocument.CustomDocumentProperties.Add Name:=sPropName, _ LinkToContent:=False, Type:=lType, Value:=sValue If Err Then 'If we still get an error, the value isn't valid for the Property Type 'e,g an invalid date was used Ni pht hnh: www.giaiphapexcel.com

96

L p trnh VBA for Word


Debug.Print "The Property " & Chr(34) & _ sPropName & Chr(34) & " couldn't be written, because " & _ Chr(34) & sValue & Chr(34) & _ " is not a valid value for the property type" End If Exit Sub ErrHandlerWriteProp: Select Case Err Case Else 'Clear the error Err.Clear 'bCustom is a boolean variable, if the code jumps to this 'errorhandler for the first time, the value for bCustom is False If Not bCustom Then 'Continue with the code after the label Proceed Resume Proceed Else 'The errorhandler was executed before because the value for 'the variable bCustom is True, therefor we know that the 'customdocumentproperty did not exist yet, jump to AddProp, 'where the property will be made Resume AddProp End If End Select End Sub

We could call the above procedure like this:


Sub Test() 'Author is a built-in property Call WriteProp(sPropName:="Author", sValue:="William Shakespeare") 'Date Updated is a custom document property Call WriteProp(sPropName:="Date Updated", sValue:="11 Mar 2001", _ lType:=msoPropertyTypeDate) End Sub

4.2. Reading Document Properties


The same principle can be used when reading Document Properties:
Function ReadProp(sPropName As String) As Variant Dim bCustom As Boolean Dim sValue As String On Error GoTo ErrHandlerReadProp Ni pht hnh: www.giaiphapexcel.com

97

L p trnh VBA for Word


'Try the built-in properties first 'An error will occur if the property doesn't exist sValue = ActiveDocument.BuiltInDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ContinueCustom: bCustom = True Custom: sValue = ActiveDocument.CustomDocumentProperties(sPropName).Value ReadProp = sValue Exit Function ErrHandlerReadProp: Err.Clear 'The boolean bCustom has the value False, if this is the first 'time that the errorhandler is runned If Not bCustom Then 'Continue to see if the property is a custom documentproperty Resume ContinueCustom Else 'The property wasn't found, return an empty string ReadProp = "" Exit Function End If End Function

We could call the function like this:


Sub Test() Dim PropVal As String PropVal = ReadProp("Author") Debug.Print PropVal PropVal = ReadProp("Date Completed") Debug.Print PropVal End Sub

5. Highlight any misspelled words, so that unrecognized words stand out prominently on a printout
Article contributed by Bill Coan
Ni pht hnh: www.giaiphapexcel.com

98

L p trnh VBA for Word


Sub HighlightMisspelledWords() Dim oWord As Range Dim StoryRange As Range For Each StoryRange In ActiveDocument.StoryRanges Application.CheckSpelling Word:=StoryRange For Each oWord In StoryRange.Words If Not Application.CheckSpelling(Word:=oWord.Text) Then oWord.HighlightColorIndex = wdYellow End If Next oWord Next StoryRange End Sub

6. Clear all highlighting from a document


Article contributed by Bill Coan The following macro clears highlighting from all words:
Sub ClearHighlightFromAllWords() Dim StoryRange As Range For Each StoryRange In ActiveDocument.StoryRanges StoryRange.HighlightColorIndex = wdNoHighlight Next StoryRange End Sub

7. Turning Allow spacing between cells off with VBA in a Word 2000 table
Article contributed by Dave Rado and Cindy Meister There seems to be no supported way in VBA to turn the Allow spacing between cells Table property off. You can set the Spacing property to 0 but the resulting table looks very different from how it looks if you de-select the Allow spacing between cells checkbox manually using the Table + Properties dialog.

7.1. Workaround 1
Selection.Tables(1).Spacing = -1 Ni pht hnh: www.giaiphapexcel.com

99

L p trnh VBA for Word makes the table look right. It's a kludge, because if you then look in the dialog you'll see that Allow spacing is still ticked but it's much better than no workaround at all!

7.2. Workaround 2
SendKeys "%s{Enter}" Dialogs(1080).Show

also works. This second workaround has the advantage that it does de-select the Allow spacing checkbox in the dialog but besides the inherent disadvantage in using SendKeys, the user will see the dialog momentarily appear and disappear.

Ni pht hnh: www.giaiphapexcel.com

100

L p trnh VBA for Word

CHAPTER 7: Working with ranges and selections (not including Tables)

1. How to select or set a Range object to the page that the cursor is on
Article contributed by Bill Coan To select the page the cursor is in
Selection.GoTo What:=wdGoToBookmark, Name:="\page"

To set a range object to the page the cursor is in:


Dim MyRange As Range Set MyRange = Selection.Range Set MyRange = MyRange.GoTo(What:=wdGoToBookmark, Name:="\page") 'you can then operate on the page without selecting it, e.g. MyRange.Delete

To set a range object to the page the range is currently in:


'Having previously set the MyRange variable to a range somewhere ... Set MyRange = MyRange.GoTo(What:=wdGoToBookmark, Name:="\page") 'you can then operate on the page without selecting it, e.g. MyRange.Delete

To set a range object to a particular page number:


Dim MyRange As Range Set MyRange = ActiveDocument.Range(0,0) Set MyRange = MyRange.GoTo(What:=wdGoToPage, Name:="4") Set MyRange = MyRange.GoTo(What:=wdGoToBookmark, Name:="\page") 'you can then operate on the page without selecting it, e.g. MyRange.Delete

2. Determine the index number of the current paragraph, table, section ...
Or of any object that has a Range property Article contributed by Dave Rado, with acknowledgments to Andrew Gabb, Jonathan West and Ibby

Ni pht hnh: www.giaiphapexcel.com

101

L p trnh VBA for Word Note that none of the following applies reliably if you are in Outline View; if you are, you need to change to some other view, such as Normal view, first.

1. In order to operate on the currently selected paragraph, table, section, etc.


The first questions is why do you need to know the index number? If you want to know in order to operate on the currently selected paragraph, table or other object, you can simply use:
Selection.CollectionName(1)

For example:
Selection.Paragraphs(1).Range.Font.Bold = True Selection.Sections(1).Headers(wdHeaderFooterPrimary).Range.Text = "Hello" Selection.Tables(1).Borders.Enable = False

All the above operate on the currently selected object. Similarly, if you were working with a range variable, you could use:
MyRange.Paragraphs(1).Range.Font.Bold = True

Etc. However if you really do need to know the index number, use the following method:

2. Get the index number, by setting a range to the start of the document
The fastest and simplest way to get the index number, by far, is to set a range from the start of the document to the end of the first selected paragraph (or other object); and then use the Count property, as follows:
MsgBox ActiveDocument.Range(0, Selection.Paragraphs(1).Range.End).Paragraphs.Count MsgBox ActiveDocument.Range(0, Selection.Sections(1).Range.End).Sections.Count MsgBox ActiveDocument.Range(0, Selection.Tables(1).Range.End).Tables.Count

Again, you could also use this method to get the index number of a range rather than of a selection, as follows:
Ni pht hnh: www.giaiphapexcel.com

102

L p trnh VBA for Word


MsgBox ActiveDocument.Range(0, MyRange.Paragraphs(1).Range.End).Paragraphs.Count

Etc. This method was suggested in the newsgroups by Andrew Gabb.

3. How to move a range variable to the end of an inserted file after using [range].InsertFile
Article contributed by Dave Rado and Will Rickards When you use MyRange.InsertFile, the range ends up collapsed at the start of the inserted text rather than at the end!

3.1. Workaround 1
Use Selection.InsertFile instead (preceded by MyRange.Select if necessary). This leaves the selection collapsed at the end of the inserted text rather than at the start.

3.2. Workaround 2
Dim rngStart As Range Dim rngEnd As Range Set rngStart = whatever (where you want the file inserted) rngStart.Collapse direction:=wdCollapseStart Set rngEnd = rngStart.Duplicate rngEnd.InsertParagraph rngStart.InsertFile "C:\Temp\test.doc" rngEnd.Characters.Last.Delete

Nuts and sledgehammers come to mind, but that works. However, the selection-based workaround is faster as well as needing less code, so you may wish to stick to using Selection.InsertFile unless you have made Word invisible, in which case it is (apparently) usually safest to avoid using selections completely; so in that situation. Workaround 2 may work better for you. This is definitely a feature of Ranges worth emailing mswish@microsoft.com about!

4. Delete any paragraph that is an exact duplicate of the preceding paragraph, using a Range object
Article contributed by Bill Coan
Ni pht hnh: www.giaiphapexcel.com

103

L p trnh VBA for Word


Dim AmountMoved As Long Dim myRange As Range 'start with first paragraph and extend range down to second Set myRange = ActiveDocument.Paragraphs(1).Range AmountMoved = myRange.MoveEnd(unit:=wdParagraph, Count:=1) 'loop until there are no more paragraphs to check Do While AmountMoved > 0 'if two paragraphs are identical, delete second one 'and add the one after that to myRange so it can be checked If myRange.Paragraphs(1).Range.Text = _ myRange.Paragraphs(2).Range.Text Then myRange.Paragraphs(2).Range.Delete AmountMoved = myRange.MoveEnd(unit:=wdParagraph, Count:=1) Else 'if two paragraphs aren't identical, add the one after 'that to my range, so it can be checked, and drop the first one, 'since it is no longer of interest. AmountMoved = myRange.MoveEnd(unit:=wdParagraph, Count:=1) myRange.MoveStart unit:=wdParagraph, Count:=1 End If Loop

5. Delete any paragraph that is an exact duplicate of the preceding paragraph, using a Selection object
Article contributed by Bill Coan
Dim AmountMoved As Long 'select first two paragraphs Selection.HomeKey unit:=wdStory Selection.MoveDown unit:=wdParagraph, Count:=1, Extend:=wdExtend AmountMoved = Selection.MoveDown(unit:=wdParagraph, Count:=1, Extend:=wdExtend) 'loop until no more paragraphs to move down to Do While AmountMoved > 0 If Selection.Paragraphs(1).Range.Text = Selection.Paragraphs(2).Range.Text Then Selection.Paragraphs(2).Range.Delete AmountMoved = Selection.MoveDown(unit:=wdParagraph, Count:=1, Extend:=wdExtend) Ni pht hnh: www.giaiphapexcel.com

104

L p trnh VBA for Word


Else AmountMoved = Selection.MoveDown(unit:=wdParagraph, Count:=1, Extend:=wdExtend) Selection.MoveStart unit:=wdParagraph, Count:=1 End If Loop Selection.HomeKey unit:=wdStory 'Return to top of doc

6. How to find out whether a range is off-screen


Article contributed by Jonathan West If the [Range].Information(wdHorizontalPositionRelativeToPage) property returns -1, then the range is not on screen. However if part of the range is on screen, it will not return -1. So if you want to check specifically whether the start or end of the range is on screen, you would need to collapse the range:
Sub CheckIfEndOfRangeOnScreen() Dim MyRange As Range, EndOfRange As Range Set MyRange = ActiveDocument.Range Set EndOfRange = MyRange.Duplicate EndOfRange.Collapse wdCollapseEnd If EndOfRange.Information(wdHorizontalPositionRelativeToPage) = -1 Then MsgBox "The end of the range is not on screen" End If End Sub

7. How to get the column number of the selection (in a document containing snaking, or newspaper-style, columns)
Article contributed by Cindy Meister Use:
With Dialogs(wdDialogFormatColumns) 'Gets the column number of the currently selected column Debug.Print .ColumnNo End With

Unfortunately this only works for selections, not for ranges.


Ni pht hnh: www.giaiphapexcel.com

105

L p trnh VBA for Word

CHAPTER 8: Working with Tables

1. Maximising the performance of Word tables


Article contributed by Dave Rado 1. As a user 2. In code To return to top, press Ctrl+Home, or use Alt + Left Arrow to Go Back)

As a user
1. Working in Normal view when you can helps, especially if you turn off Background repagination (Tools + Options + General). Whatever you do, though, tables in Word 2000 and higher are a lot slower in most respects than in Word 97 an unfortunate by-product of the new table engine created so that Word tables could be fully HTML-compatible. 2. If using Word 2000 and above, select Table | Table Properties | Options, and turn off the checkbox: Automatically resize to fit contents. As well as slowing tables down considerably, this setting gives (usually) undesirable results, but unfortunately is automatically switched on in all new tables. 3. Don't create a single row containing a large amount of text. I have seen many tables containing rows which (with non-printing characters displayed) look something like this:

Figure 1: A badly laid out table row (shown with non-printing characters and table gridlines visible) Apart from anything else, laying out table text as shown above makes it a complete nightmare to get everything to line up, which defeats the object of using a table in the first place, the great strength of tables being that they line text up automatically if used properly. But in addition, rows containing many paragraphs slow tables down. So create a separate row for each logical element of the table, as shown in Figure 2. Note that if you don't want horizontal borders between some of the rows, you don't have to have them; so not wanting borders is not a reason to add paragraphs instead of adding rows:

Ni pht hnh: www.giaiphapexcel.com

106

L p trnh VBA for Word

Figure 2: How the row in Figure 1 should have been laid out as six separate rows, but with no horizontal border between the rows 4. Break long tables up (use several smaller tables rather than one very long one), separating the sub-tables with headings. So rather than, for instance, creating something like this:

Figure 3: Avoid putting your headings inside your tables ... split your tables up into logical sub-tables instead, putting your headings outside the tables (using Heading styles), like this:

Figure 4: Put your headings outside your tables


Ni pht hnh: www.giaiphapexcel.com

107

L p trnh VBA for Word 5. Avoid using merged cells as much as possible: wherever you can get away with it, remove unwanted borders instead. 6. In Word 2000 and above, use text-wrapped tables only when really necessary. Set textwrapping to None whenever you can. For more on text-wrapped tables, see: Table basics. 7. In Word 2000 and above, if your tables contain graphics, make them inline where possible.

In code
1. If using Word 2000 and above, turn off Automatically resize to fit contents for all tables:
Selection.Tables(1).AllowAutoFit = False

Whatever you do, though, tables in Word 2000 and higher are a lot slower in most respects than in Word 97 an unfortunate by-product of the new table engine created so that Word tables could be fully HTML-compatible (but see 5. and 7. below for an astonishing exception to this rule). 2. 27 above apply to tables created with code as well. In the case of switching views and turning off background repagination, it is polite to the user to leave their settings as you found them, i.e.:
Dim ViewType As Long, PaginationSetting As Boolean ViewType = ActiveWindow.View.Type PaginationSetting = Options.Pagination 'rest of your code Options.Pagination = PaginationSetting ActiveWindow.View.Type = ViewType

Unfortunately, even with ScreenUpdating switched off, the screen flickers when you change views. The only way to prevent this is to use the the LockWindowUpdate API (which is beyond the scope of this article, but a Google search will turn up details on it). So it's only worth bothering to change views for large tables. 3. If you are putting data into a Word table using code (e.g. if you are reading it from a database), you will get much better performance if you initially put the data into the Word document as tab-delimited text, and then convert the text to a table at the very end. For example (the following code sample requires you to set a reference to DAO, and also assumes you have the Northwind sample database installed it is one of the sample databases supplied with Office, so if it is not already installed on your system, you can re-run Setup in order to install it ):
Sub GetDataIntoTable() Dim db As Database, rs As Recordset, MyRange As Range, i As Integer Set db = OpenDatabase(Name:= _ "c:\program files\microsoft office\office\samples\northwind.mdb") Set rs = db.OpenRecordset(Name:="Shippers") Set MyRange = ActiveDocument.Content MyRange.Collapse wdCollapseEnd MyRange.InsertAfter Text:=rs.Fields(1).Name & vbTab & rs.Fields(2).Name & vbCr Ni pht hnh: www.giaiphapexcel.com

108

L p trnh VBA for Word


Set MyRange = ActiveDocument.Content MyRange.Collapse wdCollapseEnd For i = 0 To rs.RecordCount - 1 'Insert the data as tab-delimited text MyRange.InsertAfter Text:=rs.Fields(1).Value & vbTab & rs.Fields(2).Value & vbCr rs.MoveNext MyRange.Collapse Direction:=wdCollapseEnd Next i rs.Close db.Close 'Now convert to table MyRange.Start = ActiveDocument.Range.Start MyRange.ConvertToTable Set db = Nothing Set rs = Nothing End Sub

If some cells in your table need to contain more than one paragraph (or to contain manual line breaks), separate those paragraphs or lines, initially, with a dummy delimiter such as a comma or a dollar sign; and then do a Find and Replace at the end (after converting the text to a table), to replace the delimiter with a paragraph mark or manual line break, as desired. For a code sample that illustrates this technique, see: How to generate a table of samples of every font on your system. 4. If for some reason you can't insert your text in tab-delimited format and convert to table at the end, then don't build up your table as you go by adding a row at a time. Instead, work out in advance the total number of rows that you'll need (e.g. by reading all your values into an array before inserting any of them in the document) and then create the entire table in one go; e.g.:
Set oTable = ActiveDocument.Tables.Add(Range:=MyRange, _ Numrows:=1000, numcolumns:=4) 'Word 2000 only: oTable.AllowAutoFit = False

5. If inserting text, use ranges rather than selections (as illustrated in the above code sample): and also, use characters such as vbCr and vbTab to allow you insert as much text as possible with a single statement again, as illustrated in the above code sample. For instance:
MyRange.InsertAfter Text:=rs.Fields(1).Value & vbTab & rs.Fields(2).Value & vbCr

... runs much faster than:


Selection.TypeText Text:=rs.Fields(1).Value Selection.TypeText Text:=vbTab Selection.TypeText Text:=rs.Fields(2).Value Selection.TypeParagraph

6. If inserting a large amount of text into the document, make sure background spelling and
Ni pht hnh: www.giaiphapexcel.com

109

L p trnh VBA for Word grammar checking are switched off. At the end of your macro, out of politeness to the user, switch the settings back on if they were on to start with. Also, if you know that the inserted text won't need to be spelling or grammar checked, you can mark the inserted range as already checked, without marking the rest of the document. (Thanks to Greg Chapman for this tip).
Dim SpellSetting As Boolean, GrammarSetting As Boolean, _ MyRange As Range SpellSetting = Options.CheckSpellingAsYouType GrammarSetting = Options.CheckGrammarAsYouType With Options .CheckSpellingAsYouType = False .CheckGrammarAsYouType = False End With 'Insert your text, e.g. Set MyRange = Selection.Range Selection.InsertFile "C:\Temp\Temp.doc" 'Or insert it from a database, whatever 'If you know that it's safe to do so, mark the inserted text as already checked, 'but don't mark the text that you didn't insert. If inserting from a database, 'set a range to the inserted text and operate on that range. 'If using Selection.InsertFile, use the following: MyRange.End = Selection.End With MyRange .SpellingChecked = True .GrammarChecked = True End With 'Rest of your code, and then at the very end: With Options .CheckSpellingAsYouType = SpellSetting .CheckGrammarAsYouType = GrammarSetting End With

7. Applying manual formatting is very resource-hungry apply predefined styles instead. 8. When cycling through table cells, never refer to a table cell by its coordinates; as that is horrendously slow, because it forces Word to calculate from scratch, for every single cell, where in the document the cell in question actually is. And don't move selection from cell to cell, as this will also slow your code down dramatically. Whilst it is much faster to cycle through the Cells collection, as in:
Sub OperateOnEveryCellUsingTableObject() Ni pht hnh: www.giaiphapexcel.com

110

L p trnh VBA for Word


Dim oCell As Cell For Each oCell In Selection.Tables(1).Range.Cells oCell.Range.Text = "Hi there" Next oCell End Sub

... a much faster method still (with screen updating switched off) is to select the table, in code, and then cycle through the cells within the selection don't ask me why this should be faster, but it is:
Sub OperateOnEveryCellInSelectedTable() Dim oCell As Cell Application.ScreenUpdating = False Selection.Tables(1).Select For Each oCell In Selection.Cells oCell.Range.Text = "Hi there" Next oCell Application.GoBack Application.ScreenUpdating = True End Sub

When I timed the above macros in Word 97 and in Word 2000, using a 350-row, 5-column table, the results were very interesting (I've rounded the results to 1 decimal place): Word 97 OperateOnEveryCellUsingTableObject OperateOnEveryCellInSelectedTable 38.5s 5.3s Word 2000 53.8s 4.5s

The above results were obtained with a table created in Word 97. If the table was created in Word 2000 (and if AllowAutoFit was switched off), then using the Table object became significantly faster in Word 2000 (though not in 97); but was still far slower than using a Selection object. If the document was created in Word 2000 and then saved in Word 97, the results were similar to the above. I have no theories to explain these results, but they are easy to reproduce. Tests by colleagues who have access to Word 2002 gave broadly similar results to Word 2000. Turning off screen updating made no difference to the speed of the OperateOnEveryCellUsingTableObject() macro, although it dramatically speeded up the OperateOnEveryCellInSelectedTable() macro. 9. When operating on specific rows, or comparing the contents of adjacent rows, use the Row object, as in the code samples at Deleting duplicate rows in a table. 10. If you want to operate on the cells in a specific table column, you can't cycle through the cells within the column's Range Ranges and Columns simply don't mix. Crazily, a table column's
Ni pht hnh: www.giaiphapexcel.com

111

L p trnh VBA for Word Range contains many cells that are not actually within the column. This must once have seemed like a good idea to someone at Microsoft, probably because they were suffering from a bad hangover at the time! There is a certain pedantic logic to it: a column's range contains all the cells starting from the top of the column, moving through the table from left to right along each row, until you get to the bottom of the column. From a usability perspective this was a nightmarish design decision, though, and well worth emailing mswish@microsoft.com about. By far the fastest way of operating on a specific column is to select it and then cycle though the selected cells, as in:
Sub OperateOnSelectedColumn3() Dim oCell As Cell Application.ScreenUpdating = False 'Select the third cell in the first row of the table Selection.Tables(1).Cell(1, 3).Select 'Select column 3 Selection.SelectColumn 'Operate on the cells in column 3 For Each oCell In Selection.Cells oCell.Range.Text = "Hi there" Next oCell Application.GoBack Application.ScreenUpdating = True End Sub

Note that you cannot safely use the Columns object to specify which column you want to select, as in:
Selection.Tables(1).Columns(3).Select

.. because that gives an error message: Cannot access individual columns in this collection because the table has mixed cell widths, either if there are any merged cells, or even if any cell anywhere in the table has a slightly different width than the rest of the cells in the same column! So for all practical purposes, the Column object is completely useless another design decision resulting from far too many Tia Marias laced with vodka, and well worth an email to mswish@microsoft.com. If there might be merged cells in row 1 of the table, you could select the third cell in the last row of the table instead, and then select the column, rather than using the first row:
Dim oRow As Row, oCell As Cell Application.ScreenUpdating = False Set oRow = Selection.Tables(1).Rows.Last oRow.Cells(3).Select Selection.SelectColumn For Each oCell In Selection.Cells oCell.Range.Text = "Hi there" Next oCell Application.GoBack Ni pht hnh: www.giaiphapexcel.com

112

L p trnh VBA for Word


Application.ScreenUpdating = True

Instead of selecting the column you could cycle through every cell in the table, operating on those cells whose ColumnIndex property matches the column you want, as follows:
Sub OperateOnColumn3UsingRanges() Dim oCell As Cell For Each oCell In Selection.Tables(1).Range.Cells If oCell.ColumnIndex = 3 Then oCell.Range.Text = "Hi there" End If Next oCell End Sub

... but this is not only much slower than selecting the column, but also, if there are any horizontally merged cells in the table, the ColumnIndex property gives undesirable results. When I timed the above macros in Word 97 and in Word 2000, using a 350-row, 5-column table, the results on my machine were as follows (I've rounded the results to 1 decimal place): Word 97 OperateOnColumn3UsingRanges OperateOnSelectedColumn3 7.3s 0.7s Word 2000 10.7s 1.1s

The above results were obtained with a table created in Word 97. If the table was created in Word 2000 (and if AllowAutoFit was switched off), then the OperateOnColumn3UsingRanges macro became significantly faster in Word 2000 (though not in 97); but was still far slower than using a Selection object. If the document was created in Word 2000 but then saved in Word 97, the results were similar to the above. As with 7. above, I have no theories to explain these results, but they are easy to reproduce. Tests by colleagues who have access to Word 2002 gave broadly similar results to Word 2000; and again, turning off screen updating made no difference to the speed of the OperateOnColumn3UsingRanges() macro, although it dramatically speeded up the OperateOnSelectedColumn3() macro. 11. If formatting the borders and shading of a table, it is far more efficient, and can speed up your code dramatically (even in Word 97), if you execute the built-in FormatBordersAndShading dialog (without displaying it), than it is to use native VBA Methods to do the formatting. This trick also greatly reduces the risk of getting Formatting too complex error messages. In essence, this is because you can execute many commands simultaneously using the dialog, whereas, using VBA methods, you have to execute one statement at a time, and wait for one to finish before the next can start.
Ni pht hnh: www.giaiphapexcel.com

113

L p trnh VBA for Word For a more detailed discussion of the principles behind this, and for some code samples to get you started, see #2 at Getting help with calling Word's built-in dialogs using VBA (and why doing so can be much more useful than you'd think), in the section: Why use built-in dialogs?. 12. If doing a great deal of formatting of tables, then even all of the above tricks combined may not prevent you from getting the odd Formatting too complex error message. Periodically clearing the Undo buffer can help prevent this:
ActiveDocument.UndoClear.

Make sure you have turned screen updating off. If that isn't sufficient, the LockWindowUpdate API (which is beyond the scope of this article, but a Google search will turn up details on it) is more efficient still, as is making the application invisible. If you still get Formatting too complex error messages, try saving the document periodically; or as a last resort (in really huge tables), periodically save the document, close it and open it again. If you use all these tricks, you will find that the performance of tables is not an issue, even in Word 2000 and higher.

2. How can I resize a table to fit the page's width?


Article contributed by Daryl Lucas, Dave Rado and Suzanne Barnhill

2.1. Word 2000


In Word 2000 you can select Table + Properties, click on the preferred width checkbox; where it says Measure in, change it to percent, and in the width spinbox, type 100%. The relative column widths are preserved in the resized table. Or if you want to do it programmatically, you can use:
Selection.Tables(1).PreferredWidthType = wdPreferredWidthPercent Selection.Tables(1).PreferredWidth = 100

2.2. Word 97
In Word 97, you can select the entire table, and on the Column tab of Table | Cell Height and Width, type "a" and press the down arrow. The box will fill with the word Auto; OK out, and the table is resized to the margin width. Unfortunately (unlike Word 2000), the columns in the resized table are all equal widths. Alternatively, if you click in any cell (with nothing selected) and follow the same procedure, the column which the insertion point is in will be resized so that the table fits the page margins without the other column widths being affected.

Ni pht hnh: www.giaiphapexcel.com

114

L p trnh VBA for Word If you want to preserve the relative column widths when you resize the table, (as one generally does), you'll need a macro to do the job:
Sub MakeTableFitPageSize() Dim Dim Dim Dim Dim Dim Dim myTable As Table OriginalRange As Range oRow As Row oCell As Cell UsableWidth As Single TableWidth As Single CellNo As Long

If Selection.Tables.Count = 0 Then MsgBox "Please put your cursor inside a table and try again", vbInformation Exit Sub End If Application.ScreenUpdating = False System.Cursor = wdCursorWait Set OriginalRange = Selection.Range Set myTable = Selection.Tables(1) myTable.Rows.SetLeftIndent _ LeftIndent:=0, RulerStyle:=wdAdjustNone 'Calculate usable width of page With ActiveDocument.PageSetup UsableWidth = .PageWidth - .LeftMargin - .RightMargin End With 'Calculate width of top row, on assumption this will be 'the same as table width On Error Resume Next For CellNo = 1 To myTable.Rows(1).Cells.Count If Err = 5991 Then MsgBox "This macro doesn't work with tables that have vertically merged cells", _ vbInformation GoTo CleanUp Else If Err Then MsgBox Err.Description, vbInformation GoTo CleanUp End If TableWidth = TableWidth + myTable.Rows(1).Cells(CellNo).Width Next CellNo Ni pht hnh: www.giaiphapexcel.com

115

L p trnh VBA for Word


On Error Goto 0 'Calculate and assign width of each cell in each row, such that the cell width relative 'to the table's width stays the same as before. Do it for each row individually rather than 'for a column at a time- otherwise the macro won't work 'if any of the rows contain horizontally merged cells For Each oRow In myTable.Rows For Each oCell In oRow.Cells oCell.Width = (oCell.Width) * (UsableWidth / TableWidth) Next oCell Next oRow OriginalRange.Select Cleanup: 'Clear variables from memory Set myTable = Nothing Set OriginalRange = Nothing Set oRow = Nothing Set oCell = Nothing UsableWidth = 0 TableWidth = 0 CellNo = 0 System.Cursor = wdCursorNormal Application.ScreenUpdating = True End Sub

3. Deleting duplicate rows in a table


Article contributed by Ibby

3.1. Method 1
The following macros will delete duplicate rows (rows with the same text in column 1) in a sorted table. The first is case-sensitive (ie: it will not delete rows where the text is the same but the case of the text is different). The second will delete duplicates even if the case of the text is different.
Option Explicit Public Sub DeleteDuplicateRows() ' Deletes Rows with duplicate text in first column. Ni pht hnh: www.giaiphapexcel.com

116

L p trnh VBA for Word


' It will not delete identical entries if the case ' of the text is different. Dim Dim Dim Dim oTable As Table oRow As Range oNextRow As Range i As Long

' Specify which table you want to work on. Set oTable = ActiveDocument.Tables(1) ' Set an object variable to the first row. Set oRow = oTable.Rows(1).Range ' Turn off screen updating reduces screen flicker ' and lets the code run faster Application.ScreenUpdating = False For i = 1 To oTable.Rows.Count - 1 ' Set an object variable to the next row. Set oNextRow = oRow.Next(wdRow) ' Compare the text in the first column of the two rows. If oRow.Cells(1).Range = oNextRow.Cells(1).Range Then ' If text is identical, delete the second row oNextRow.Rows(1).Delete Else ' If not identical, move to the next row. Set oRow = oNextRow End If Next i ' Turn screen updating back on. Application.ScreenUpdating = True End Sub -------------------------------------Option Explicit Public Sub DeleteDuplicateRows() ' Deletes Rows with duplicate text in first column. ' It will delete identical entries even if the case ' of the text is different. Dim Dim Dim Dim Dim Dim oTable As Table oRow As Range oNextRow As Range i As Long txtCell As String txtCellNext As String

Ni pht hnh: www.giaiphapexcel.com

117

L p trnh VBA for Word


Set oTable = ActiveDocument.Tables(1) Set oRow = oTable.Rows(1).Range Application.ScreenUpdating = False For i = 1 To oTable.Rows.Count - 1 Set oNextRow = oRow.Next(wdRow) txtCell = LCase(oRow.Cells(1).Range.Text) txtCellNext = LCase(oNextRow.Cells(1).Range.Text) If txtCell = txtCellNext Then oNextRow.Rows(1).Delete Else Set oRow = oNextRow End If Next i Application.ScreenUpdating = True End Sub

3.2. Method 2
If you only want to delete rows that have identical text to each other (in all columns), then, depending on the size of the table; and assuming it contains no merged cells; the fastest way is often to convert the table to text, then use a wildcard Find & Replace, before converting the text back to a table. This method is described in the article: Finding and replacing characters using wildcards, in the section Example 4: Duplicate paragraphs (and rows).

4. Detect whether a table cell is empty


Article contributed by Bill Coan

4.1. Method 1
Use the range object to detect empty cells based on the idea that an empty cell consists of a paragraph mark followed by Chr(7).
Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row Ni pht hnh: www.giaiphapexcel.com

118

L p trnh VBA for Word


For Each oRow In Selection.Tables(1).Rows For Each oCell In oRow.Cells If oCell.Range.Text = Chr(13) & Chr(7) Then MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Next oCell Next oRow End Sub

4.2. Method 2
Use a range variable, set it to mark each cell's range; move the end of the range so that the end of cell marker and paragraph marker are not included in the range; (to do this you only have to move it by one character); and get the text within the range. If the cell is empty there will be no text within the range
Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row Dim MyRange As Range For Each oRow In Selection.Tables(1).Rows For Each oCell In oRow.Cells Set MyRange = oCell.Range MyRange.End = MyRange.End - 1 If Len(MyRange.Text) = 0 Then MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Next oCell Next oRow End Sub

4.3. Method 3
Here's some code that is similar, but which selects each cell before announcing whether it is empty. Sub CheckTableCells() Dim oCell As Cell Dim oRow As Row Dim MyRange As Range For Each oRow In Selection.Tables(1).Rows
Ni pht hnh: www.giaiphapexcel.com

119

L p trnh VBA for Word For Each oCell In oRow.Cells If Selection.Text = Chr(13) & Chr(7) Then oCell.Select MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty." End If Next oCell Next oRow End Sub

5. Delete all rows of a table that contain a particular text string in the first column
Article contributed by Bill Coan
Sub DeleteRows() Dim TargetText As String Dim oRow As Row If Selection.Information(wdWithInTable) = False Then Exit Sub TargetText = InputBox$("Enter target text:", "Delete Rows") For Each oRow In Selection.Tables(1).Rows If oRow.Cells(1).Range.Text = TargetText & vbCr & Chr(7) Then oRow.Delete Next oRow End Sub

6. Determine the index number of the current paragraph, table, section ...
Or of any object that has a Range property Article contributed by Dave Rado, with acknowledgments to Andrew Gabb, Jonathan West and Ibby Note that none of the following applies reliably if you are in Outline View; if you are, you need to change to some other view, such as Normal view, first.

1. In order to operate on the currently selected paragraph, table, section, etc.


The first questions is why do you need to know the index number? If you want to know in
Ni pht hnh: www.giaiphapexcel.com

120

L p trnh VBA for Word order to operate on the currently selected paragraph, table or other object, you can simply use:
Selection.CollectionName(1)

For example:
Selection.Paragraphs(1).Range.Font.Bold = True Selection.Sections(1).Headers(wdHeaderFooterPrimary).Range.Text = "Hello" Selection.Tables(1).Borders.Enable = False

All the above operate on the currently selected object. Similarly, if you were working with a range variable, you could use:
MyRange.Paragraphs(1).Range.Font.Bold = True

Etc. However if you really do need to know the index number, use the following method:

2. Get the index number, by setting a range to the start of the document
The fastest and simplest way to get the index number, by far, is to set a range from the start of the document to the end of the first selected paragraph (or other object); and then use the Count property, as follows:
MsgBox ActiveDocument.Range(0, Selection.Paragraphs(1).Range.End).Paragraphs.Count MsgBox ActiveDocument.Range(0, Selection.Sections(1).Range.End).Sections.Count MsgBox ActiveDocument.Range(0, Selection.Tables(1).Range.End).Tables.Count

Again, you could also use this method to get the index number of a range rather than of a selection, as follows:
MsgBox ActiveDocument.Range(0, MyRange.Paragraphs(1).Range.End).Paragraphs.Count

Etc. This method was suggested in the newsgroups by Andrew Gabb.


Ni pht hnh: www.giaiphapexcel.com

121

L p trnh VBA for Word

7. Apply changes to all cells in a table


Article contributed by Bill Coan Drill down to the cells in the table's range, as follows, and set the various properties as desired:
For Each oCell In oTable.Range.Cells oCell.Width = InchesToPoints(1) oCell.Shading.BackgroundPatternColorIndex = wdBlue oCell.Range.Font.Name = "Arial" oCell.Range.Font.Size = 20 Next oCell

8. Apply changes to an individual cell in a table


Article contributed by Bill Coan Drill down to the desired cell. In this example, Cell(1, 1) refers to the cell in row 1, column 1:
With ActiveDocument.Tables(1).Cell(1, 1) .Width = InchesToPoints(1) .Shading.BackgroundPatternColorIndex = wdBlue .Range.Font.Name = "Arial" .Range.Font.Size = 20 End With

9. Select all but the first two cells in a table column


Article contributed by Bill Coan After selecting the column, set the start of the selection range to the start of the third cell in the selection. Wrap the code inside an If statement to make sure the selection is within a table. (That way, if not in a table, nothing will happen).
If Selection.Information(wdWithInTable) Then Selection.Columns(1).Select Selection.SetRange _ Start:=Selection.Cells(3).Range.Start, _ End:=Selection.End End If

Ni pht hnh: www.giaiphapexcel.com

122

L p trnh VBA for Word

10. Display in a message box the contents of each cell in a table column
Article contributed by Bill Coan
Sub DisplayTextFromCellsInColumn1() Dim myRange As Range For Each oCell In Selection.Tables(1).Columns(1).Cells Set myRange = oCell.Range myRange.SetRange Start:=myRange.Start, End:=myRange.End - 1 MsgBox myRange.Text Next oCell End Sub

11. Select a range of cells within a table


Article contributed by Bill Coan Use the Selection.SetRange statement. The sample provided here selects all cells from row 2 column 2 to row 3 column 3.
If Selection.Information(wdWithInTable) = False Then Exit Sub Selection.SetRange _ Start:=Selection.Tables(1).Cell(2, 2).Range.Start, _ End:=Selection.Tables(1).Cell(3, 3).Range.End

12. Select all rows of a table except the first row


Article contributed by Bill Coan
ActiveDocument.Tables(1).Select Selection.SetRange _ Start:=Selection.Rows(2).Range.Start, _ End:=Selection.End

Ni pht hnh: www.giaiphapexcel.com

123

L p trnh VBA for Word

13. How to centre a left-justified table (or left or right-justify a centred one)
Article contributed by Dave Rado Word 97 Click anywhere in the table, and on the Table menu select Cell Height and Width. (Why Cell Height and Width, you may ask? Good question. Word 2000 is more logical). Where it says Indent from Left, make sure it's set to 0. Where it says Alignment, set it to Left, Centre, or Right as desired. Word 2000 Click anywhere in the table, and on the Table menu select Table Properties. Where it says Indent from Left, make sure it's set to 0. Where it says Alignment, set it to Left, Centre, or Right as desired. Using VBA In both versions of Word, the VBA for this is:
With Selection.Tables(1).Rows .LeftIndent = 0 .Alignment = wdAlignRowCenter 'or set whichever alignment you want End With

14. How to get the Rowspan and Colspan of a table cell using VBA
Article contributed by Klaus Linke and Jeff Hall

14.1. Rowspan
You can get the number of spanned rows, if you select the cell using myCell.Select, and then use
RowSpan = (Selection.Information(wdEndOfRangeRowNumber) - _ Selection.Information(wdStartOfRangeRowNumber)) + 1

Unfortunately, this method does not work if you use ranges, so:

Ni pht hnh: www.giaiphapexcel.com

124

L p trnh VBA for Word


Dim MyRange As Range Set MyRange = ActiveDocument.Tables(1).Cell(1, 1).Range RowSpan = (MyRange.Information(wdEndOfRangeRowNumber) - _ MyRange.Information(wdStartOfRangeRowNumber)) + 1

does not work (this is a bug). Also, this method does not work on a table cell that has been selected manually with the mouse, and nor does it work if you use Selection.Expand Unit:=wdCell to select the cell. You have to use MyCell.Select, e.g.:
Dim MyCell As Cell For Each MyCell In ActiveDocument.Tables(1).Range.Cells MyCell.Select Msgbox "Rowspan = " & _ (Selection.Information(wdEndOfRangeRowNumber) - _ Selection.Information(wdStartOfRangeRowNumber)) + 1 Next MyCell

14.2. Colspan
There doesn't seem to be a straightforward way to determine the number of columns that a merged cell spans. But the best workaround seems to be as follows: 1. 2. 3. 4. Calculate the total width of the table (in points). Get the width of current cell in points. Convert the cell width to a percentage of the table width (eg 27%). use <TD COLSPAN=27 WIDTH=27%>.

This effectively breaks a table into 100 vertical columns, each 1% of the table width. The browser can easily display the cells accurately even when there are staggered or horizontally merged cells. No need for a load of complicated logic. Note that you should not use COLS=100 in the <TABLE> definition because (surpise surpise) Netscape does not like it!!

15. Change the behavior of the TAB key inside a table cell
Article contributed by Bill Coan Note When the cursor is inside an unprotected table and you press the Tab key, Word runs a built-in routine called NextCell. If you create a custom version of this routine, Word will run your version instead of the built-in version. If the table is in a section of a document protected for forms, Word runs a built-in routine that lies beyond the reach of Visual Basic for Applications.
Ni pht hnh: www.giaiphapexcel.com

125

L p trnh VBA for Word Solution Use a macro to move the cursor downward through each column when the tab key is pressed (or upward when Shift+Tab is pressed). Design the macro so that, when the cursor reaches the bottom of a column, it moves to the top of the next column. Caveats The following routines have been tested only on tables where all rows have the same number of columns and all columns have the same number of rows. Using them disables the ability to create new rows by tabbing. 1. 2. Click Macro on the Tools menu, then click Macros... on the submenu. Under Macro Name, enter NextCell, then click Create Word will show the following code, which represents the built-in NextCell routine:
Sub NextCell() ' ' NextCell Macro ' Moves to the next table cell ' Selection.MoveRight Unit:=wdCell End Sub

3.

Replace the built-in code with the following code:


Sub NextCell() Dim NeedToSelect As Long, CurrentRow As Long, CurrentColumn As Long Do While Selection.Information(wdStartOfRangeColumnNumber) > _ Selection.Information(wdMaximumNumberOfColumns) Selection.MoveLeft NeedToSelect = 1 Loop If NeedToSelect = 1 Then Selection.Cells(1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Exit Sub End If CurrentRow = Selection.Information(wdStartOfRangeRowNumber) CurrentColumn = Selection.Information(wdStartOfRangeColumnNumber) If CurrentRow < Selection.Information(wdMaximumNumberOfRows) Then Selection.Tables(1).Cell(CurrentRow + 1, CurrentColumn).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 ElseIf CurrentColumn < Selection.Information(wdMaximumNumberOfColumns) Then Selection.Tables(1).Cell(1, CurrentColumn + 1).Select

Ni pht hnh: www.giaiphapexcel.com

126

L p trnh VBA for Word


Selection.MoveEnd unit:=wdCharacter, Count:=-1 Else Selection.Tables(1).Cell(1, 1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 End If System.Cursor = wdCursorNormal End Sub

4.

Similarly create a PrevCell macro as with the following code, to intercept pressing Shift+Tab:
Sub PrevCell() Dim NeedToSelect As Long, CurrentRow As Long, CurrentColumn As Long, _ NumRows As Long, NumCols As Long Do While Selection.Information(wdStartOfRangeColumnNumber) > _ Selection.Information(wdMaximumNumberOfColumns) Selection.MoveLeft NeedToSelect = 1 Loop If NeedToSelect = 1 Then Selection.Cells(1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Exit Sub End If NumRows = Selection.Information(wdMaximumNumberOfRows) NumCols = Selection.Information(wdMaximumNumberOfColumns) CurrentRow = Selection.Information(wdStartOfRangeRowNumber) CurrentColumn = Selection.Information(wdStartOfRangeColumnNumber) If CurrentRow > 1 Then Selection.Tables(1).Cell(CurrentRow - 1, CurrentColumn).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 ElseIf CurrentColumn > 1 Then Selection.Tables(1).Cell(NumRows, CurrentColumn - 1).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 Else Selection.Tables(1).Cell(NumRows, NumCols).Select Selection.MoveEnd unit:=wdCharacter, Count:=-1 End If System.Cursor = wdCursorNormal End Sub

5. 6. 7.

Select Save Normal on the File menu. Select Close and Return to MS Word on the File menu. Position the cursor in a table and press the TAB key 127

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word 8. 9. If not satisfied with the results, modify the code as desired. To return the TAB key to its normal functionality, delete or rename the macros.

16. Turning Allow spacing between cells off with VBA in a Word 2000 table
Article contributed by Dave Rado and Cindy Meister There seems to be no supported way in VBA to turn the Allow spacing between cells Table property off. You can set the Spacing property to 0 but the resulting table looks very different from how it looks if you de-select the Allow spacing between cells checkbox manually using the Table + Properties dialog.

16.1. Workaround 1
Selection.Tables(1).Spacing = -1

makes the table look right. It's a kludge, because if you then look in the dialog you'll see that Allow spacing is still ticked but it's much better than no workaround at all!

16.2. Workaround 2
SendKeys "%s{Enter}" Dialogs(1080).Show

also works. This second workaround has the advantage that it does de-select the Allow spacing checkbox in the dialog but besides the inherent disadvantage in using SendKeys, the user will see the dialog momentarily appear and disappear.

Ni pht hnh: www.giaiphapexcel.com

128

L p trnh VBA for Word

CHAPTER 9: Working with strings, dates and Find & Replace

1. How do I return the date of the previous month using VBA?


Article contributed by Beth Melton, Ibby and Dave Rado VBA contains many Date functions that you can utilize for Date calculations. For getting information such as the equivalent date in the previous month, (e.g. to return November 30 if the current date is December 31), the simplest method is to use the DateAdd function, as follows:
MsgBox DateAdd("m", -1, Date)

The above returns the date using your system's short date format - i.e. 30/11/2000 (UK, AUS), or 11/30/2000 (US). To specify the format you want the date to be returned in, use the Format function. For instance, if the current date is 31 December 2000, the following will return 30 November 2000:
MsgBox Format(DateAdd("m", -1, Date), "dd mmmm yyyy")

If you wanted a macro to insert the date in your document, it would look something like:
Sub PrevMonth() Dim mBefore As Date mBefore = Format(DateAdd("m", -1, Date), "dd mmmm yyyy") Selection.InsertBefore mBefore End Sub

Here are some more examples of using DateAdd combined with Format:
MsgBox Format(DateAdd("m", -1, "31 July 2000"), "dd mmmm yyyy")

Returns 30 June 2000


MsgBox Format(DateAdd("m", -1, "31/1/2000"), "dd mmmm yyyy")

Returns 31 December 1999. Some useful date functions worth exploring in other contexts include DateDiff, DatePart, DateSerial, Day, Month, Year, and IsDate (all covered in help). IsDate (which checks whether a string is a valid date) is particularly useful for things like UserForms where you may want to force the user to type a valid date into a certain textbox.

Ni pht hnh: www.giaiphapexcel.com

129

L p trnh VBA for Word

2. Using a macro to replace text where ever it appears in a document


A collaborative effort of MVPs Doug Robbins and Greg Maxey with enhancements by Peter Hewett and Jonathan West Using the Find or Replace utility on the Edit menu you can find or replace text "almost" anywhere it appears in the document. If you record that action however, the scope or "range" of the resulting recorded macro will only act on the text contained in the body of the document (or more accurately, it will only act on the part of the document that contains the insertion point). This means that if the insertion point is located in the main body of the document when your macro is executed it will have no effect on text that is in the headers or footers of the document, for example, or in a textbox, footnotes, or any other area that is outside the main body of the document. Even the Find and Replace utility has a shortcoming. For example, text in a textbox located in a header or footer is outside the scope of the Find and Replace utility search range. Both issues are well worth sending an email to mswish@microsoft.com. To use a macro to find or replace text anywhere in a document, it is necessary to loop through each individual part of the document. In VBA, these parts are called StoryRanges. Each StoryRange is identified by a unique wdStoryType constant. There are eleven different wdStoryType constants that can form the StoryRanges (or parts) of a document (ok, a few more in later versions of Word, but they have no bearing in this discussion). Simple documents may contain only one or two StoryRanges, while more complex documents may contain more. The wdStoryTypes that have a role in find and replace are:
wdCommentsStory, wdEndnotesStory, wdEvenPagesFooterStory, wdEvenPagesHeaderStory, wdFirstPageFooterStory, wdFirstPageHeaderStory, wdFootnotesStory, wdMainTextStory, wdPrimaryFooterStory, wdPrimaryHeaderStory, and wdTextFrameStory.

The complete code to find or replace text anywhere is a bit complex. Accordingly, lets take it a step at a time to better illustrate the process. In many cases the simpler code is sufficient for getting the job done.

2.1. Step 1
The following code loops through each StoryRange in the active document and replaces the specified .Text with .Replacement.Text:
Sub FindAndReplaceFirstStoryOfEachType() Dim rngStory As Range For Each rngStory In ActiveDocument.StoryRanges With rngStory.Find .Text = "find text" .Replacement.Text = "I'm found" .Wrap = wdFindContinue

Ni pht hnh: www.giaiphapexcel.com

130

L p trnh VBA for Word


.Execute Replace:=wdReplaceAll End With Next rngStory End Sub

(Note for those already familiar with VBA: whereas if you use Selection.Find, you have to specify all of the Find and Replace parameters, such as .Forward = True, because the settings are otherwise taken from the Find and Replace dialog's current settings, which are sticky, this is not necessary if using [Range].Find where the parameters use their default values if you don't specify their values in your code). The simple macro above has shortcomings. It only acts on the "first" StoryRange of each of the eleven StoryTypes (i.e., the first header, the first textbox, and so on). While a document only has one wdMainTextStory StoryRange, it can have multiple StoryRanges in some of the other StoryTypes. If, for example, the document contains sections with un-linked headers and footers, or if it contains multiple textboxes, there will be multiple StoryRanges for those StoryTypes and the code will not act upon the second and subsequent StoryRanges. To even further complicate matters, if your document contains unlinked headers or footers and one of the headers or footers are empty then VBA can have trouble "jumping" that empty header or footer and process subsequent headers and footers.

2.2. Step 2
To make sure that the code acts on every StoryRange in each each StoryType, you need to:

Make use of the NextStoryRange method Employ a bit of VBA "trickery" as provided by Peter Hewett to bridge any empty unlinked headers and footers.

Public Sub FindReplaceAlmostAnywhere() Dim rngStory As Word.Range Dim lngJunk As Long 'Fix the skipped blank Header/Footer problem as provided by Peter Hewett lngJunk = ActiveDocument.Sections( 1 ).Headers( 1 ).Range.StoryType 'Iterate through all story types in the current document For Each rngStory In ActiveDocument.StoryRanges 'Iterate through all linked stories Do With rngStory.Find .Text = "find text" .Replacement.Text = "I'm found" .Wrap = wdFindContinue .Execute Replace:=wdReplaceAll End With 'Get next linked story (if any) Set rngStory = rngStory.NextStoryRange Loop Until rngStory Is Nothing Next End Sub

There is one remaining problem. Just like with the Find and Replace utility, the code above can miss any text that is contained in one StoryType/StoryRange nested in a different StoryType/StoryRange.
Ni pht hnh: www.giaiphapexcel.com

131

L p trnh VBA for Word While this problem does not occur with a nested StoryType/StoryRange in the wdMainTextStory StoryRange, it does occur in header and footer type StoryRanges. An example is textbox that is located in a header or footer.

2.3. Step 3
Fortunately Jonathan West provided a work around for the problem of such nested StoryRanges. The work around makes use of the fact that Textboxes and other Drawing Shapes are contained in a documents ShapeRange collection. We can therefore check the ShapeRange in each of the six header and footer StoryRanges for the presence of Shapes. If a Shape is found, we then check each Shape for the presence of the text, and finally, if the Shape contains text we set our search range to that Shape's .TextFrame.TextRange. This final macro contains all of the code to find and replace text anywhere in a document. A few enhancements have been added to make it easier to apply the desired find and replace text strings. Note: It is important to convert the code text to plain text before you paste: if you paste directly from a web browser, spaces are encoded as non-breaking spaces, which are not "spaces" to VBA and will cause compile- or run-time errors. Also: Be careful of the long lines in this code. When you paste this code into the VBA Editor, there should be NO red visible anywhere in what you pasted. If there is, try carefully joining the top red line with the one below it (without deleting any visible characters.
Public Sub FindReplaceAnywhere() Dim rngStory As Word.Range Dim pFindTxt As String Dim pReplaceTxt As String Dim lngJunk As Long Dim oShp As Shape pFindTxt = InputBox("Enter the text that you want to find." _ , "FIND" ) If pFindTxt = "" Then MsgBox "Cancelled by User" Exit Sub End If TryAgain: pReplaceTxt = InputBox( "Enter the replacement." , "REPLACE" ) If pReplaceTxt = "" Then If MsgBox( "Do you just want to delete the found text?", _ vbYesNoCancel) = vbNo Then GoTo TryAgain ElseIf vbCancel Then MsgBox "Cancelled by User." Exit Sub End If End If 'Fix the skipped blank Header/Footer problem lngJunk = ActiveDocument.Sections( 1 ).Headers( 1 ).Range.StoryType 'Iterate through all story types in the current document For Each rngStory In ActiveDocument.StoryRanges 'Iterate through all linked stories Do SearchAndReplaceInStory rngStory, pFindTxt, pReplaceTxt On Error Resume Next

Ni pht hnh: www.giaiphapexcel.com

132

L p trnh VBA for Word


Select Case rngStory.StoryType Case 6 , 7 , 8 , 9 , 10 , 11 If rngStory.ShapeRange.Count > 0 Then For Each oShp In rngStory.ShapeRange If oShp.TextFrame.HasText Then SearchAndReplaceInStory oShp.TextFrame.TextRange, _ pFindTxt, pReplaceTxt End If Next End If Case Else 'Do Nothing End Select On Error GoTo 0 'Get next linked story (if any) Set rngStory = rngStory.NextStoryRange Loop Until rngStory Is Nothing Next End Sub Public Sub SearchAndReplaceInStory(ByVal rngStory As Word.Range, _ ByVal strSearch As String , ByVal strReplace As String ) With rngStory.Find .ClearFormatting .Replacement.ClearFormatting .Text = strSearch .Replacement.Text = strReplace .Wrap = wdFindContinue .Execute Replace:=wdReplaceAll End With End Sub

3. How to Find & ReplaceAll on a batch of documents in the same folder


Article contributed by Ibby The following code, if stored in a Global template, will perform a Find & ReplaceAll in all of the documents in a specified folder. The FindReplace dialog is displayed for the first document only. The user sets the parameters in the dialog and presses Replace All and then Close. The user is then asked whether to process all of the files in the specified directory if Yes, the rest of the files are processed with the settings as entered in the original FindReplace dialog.
Option Explicit Public Sub BatchReplaceAll() Dim Dim Dim Dim Dim FirstLoop As Boolean myFile As String PathToUse As String myDoc As Document Response As Long

Ni pht hnh: www.giaiphapexcel.com

133

L p trnh VBA for Word


PathToUse = "C:\Test\" 'Error handler to handle error generated whenever 'the FindReplace dialog is closed On Error Resume Next 'Close all open documents before beginning Documents.Close SaveChanges:=wdPromptToSaveChanges 'Boolean expression to test whether first loop 'This is used so that the FindReplace dialog will 'only be displayed for the first document FirstLoop = True 'Set the directory and type of file to batch process myFile = Dir$(PathToUse & "*.doc") While myFile <> "" 'Open document Set myDoc = Documents.Open(PathToUse & myFile) If FirstLoop Then 'Display dialog on first loop only Dialogs(wdDialogEditReplace).Show FirstLoop = False Response = MsgBox("Do you want to process " & _ "the rest of the files in this folder", vbYesNo) If Response = vbNo Then Exit Sub Else 'On subsequent loops (files), a ReplaceAll is 'executed with the original settings and without 'displaying the dialog box again With Dialogs(wdDialogEditReplace) .ReplaceAll = 1 .Execute End With End If 'Close the modified document after saving changes myDoc.Close SaveChanges:=wdSaveChanges Ni pht hnh: www.giaiphapexcel.com

134

L p trnh VBA for Word


'Next file in folder myFile = Dir$() Wend End Sub

If you want to perform the Find & ReplaceAll in all subfolders as well, use the FileSearch object instead of Dir. Dir is significantly faster than FileSearch when searching a single directory and it's also simpler to code; but when searching all subdirectories as well, it's simplest to use FileSearch, e.g.:
Option Explicit Public Sub BatchReplaceAll() Dim Dim Dim Dim Dim Dim FirstLoop As Boolean myFile As String PathToUse As String myDoc As Document Response As Long i As Long

PathToUse = "C:\Test\" 'Error handler to handle error generated whenever 'the FindReplace dialog is closed On Error Resume Next 'Close all open documents before beginning Documents.Close SaveChanges:=wdPromptToSaveChanges 'Boolean expression to test whether first loop 'This is used so that the FindReplace dialog will 'only be displayed for the first document FirstLoop = True 'Set the directory and type of file to batch process With Application.FileSearch .NewSearch .LookIn = PathToUse .SearchSubFolders = True .FileName = "*.doc" .MatchTextExactly = True .FileType = msoFileTypeAllFiles If .Execute() Then Ni pht hnh: www.giaiphapexcel.com

135

L p trnh VBA for Word


For i = 1 To .FoundFiles.Count 'Open document Set myDoc = Documents.Open(.FoundFiles(i)) If FirstLoop Then 'display dialog on first loop only Dialogs(wdDialogEditReplace).Show FirstLoop = False Response = MsgBox("Do you want to process " & _ "the rest of the files in this folder", vbYesNo) If Response = vbNo Then Exit Sub Else 'On subsequent loops (files), a ReplaceAll is 'executed with the original settings and without 'displaying the dialog box again With Dialogs(wdDialogEditReplace) .ReplaceAll = 1 .Execute End With End If 'Close the modified document after saving changes myDoc.Close SaveChanges:=wdSaveChanges Next i End If End With End Sub

4. Clear settings from Find and Replace dialog to prevent unexpected results from future Find or Replace operations
Article contributed by Bill Coan To prevent the user from having to change the settings in the Find and Replace dialog after running your macros, make sure you call the following procedure after doing any Find and Replace operations in VBA. 136 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word


Sub ClearFindAndReplaceParameters() With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = "" .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False End With End Sub

5. Flush bad karma from Word's find facility after an unsuccessful wildcard search
Article contributed by Bill Coan There is a bug in Word that means any Find or Find or Replace operation in VBA that follows an unsuccessful wildcard search may sometimes fail. The following code fixes this problem.
Sub WildcardSearch() Dim myWorkingRange As Range Set myWorkingRange = ActiveDocument.Range 'call a routine that removes any previous settings from the find dialog Call ClearFindAndReplaceParameters myWorkingRange.Find.Execute FindText:= "[!^013]" , _ MatchWildcards:= True , Forward:= True 'exit sub if search is successful If myWorkingRange.Find.Found Then MsgBox "tell the user something" Call ClearFindAndReplaceParameters Exit Sub End If 'this is a dummy search because otherwise 'subsequent searches will break down 'somehow this search flushes the bad karma 'and lets subsequent searches function myWorkingRange.Find.Execute FindText:= "^p" , _ MatchWildcards:= False

Ni pht hnh: www.giaiphapexcel.com

137

L p trnh VBA for Word


'call a routine that removes all settings from the find dialog 'so future users of the dialog won't get strange results Call ClearFindAndReplaceParameters End Sub

Sub ClearFindAndReplaceParameters() With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = "" .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False End With End Sub

Note: There is no simple way to reproduce this bug. In a macro that makes dozens of wildcard searches, the bug will eventually manifest itself, but I don't know of a way to force the bug to appear. The good news is that if you use the above workaround, the bug will never appear.

6. How to prevent the built-in BrowseNext and RepeatFind commands from creating bad karma for wildcard searches
Article contributed by Claudio Faria, Klaus Linke, Dave Rado and Bill Coan The following bug is easy to reproduce in Word versions 97 through 2002: 1. Using Edit + Find, perform a Find operation of any kind. 2. Use Shift+F4 or Ctrl+PgDn or the Browse button to repeat the last find operation. Continue until you reach the end of the document, but click No when asked if you want to continue at the start of the document. 3. Do a wildcard search of any kind from VBA. The search will fail.

6.1. Workaround 1: Intercepting the RepeatFind, BrowseNext and BrowsePrev commands


Put the following code into an add-in (or of it's only for your own personal use, you could put it into Normal.dot, if you prefer). It automatically intercepts the built-in RepeatFind (Shift+F4), BrowseNext (Ctrl+PgDn) and BrowsePrev (Ctrl+PgUp) commands, and prevents the bug from ever rearing its ugly head.
Ni pht hnh: www.giaiphapexcel.com

138

L p trnh VBA for Word


Sub BrowseNext() Select Case Application.Browser.Target Case wdBrowseFind With Selection.Find .Forward = True .Execute Wrap:=wdFindAsk End With Case Else Application.Browser.Next End Select End Sub

Sub BrowsePrev() Select Case Application.Browser.Target Case wdBrowseFind With Selection.Find .Forward = False .Execute Wrap:=wdFindAsk End With Case Else Application.Browser.Previous End Select End Sub

Sub RepeatFind() Select Case Application.Browser.Target Case wdBrowseFind Selection.Find.Execute Wrap:=wdFindAsk Case Else WordBasic.ToolsMacro Name:="RepeatFind", Run:=1, Show:=2 End Select End Sub

In Word 2000+, if the browse mode is set to Find, the BrowseNext and BrowsePrev commands themselves invoke the RepeatFind command; so strictly speaking, in those versions of Word, it is only necessary to intercept RepeatFind. But if you want compatibility with Word 97, you need to intercept all three commands, as shown above.

6.2. Workaround 2: Executing the Find & Replace dialog to clear the bug
139

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word The following workaround is included mainly for the sake of completeness (and for those who, for whatever reason, don't want to intercept Word commands). Workaround 1 is far more elegant. You can fix the bug using the FixWildcardBug macro shown below.
Sub Test() Application.ScreenUpdating = False 'Call a routine that removes any previous settings from the find dialog Call ClearFindAndReplaceParameters 'Now call the fix Call FixWildcardBug 'Call it a second time just in case (because the SendKeys statement is notoriously flaky) Call FixWildcardBug 'Now it's safe to do your wildcard search, e.g.: With Selection.Find .Text = "(" & ChrW( 8220 ) & ")(*)(" & ChrW( 8221 ) & ")" .Replacement.Text = "" .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = True .Execute End With Call ClearFindAndReplaceParameters Application.ScreenUpdating = True End Sub

Sub FixWildcardBug() With Selection.Find .Execute MatchWildcards:= True , FindText:= "?" Selection.Collapse Direction:=wdCollapseStart If Not .Found Then 'Call and execute the Replace dialog - not using the wdDialog constant, _ ' which is buggy, but by executing the command button instead CommandBars.FindControl(ID:= 313 ).Execute SendKeys String:= "{ENTER}" , Wait:= True SendKeys String:= "{ESC}" , Wait:= True End If Selection.Collapse Direction:=wdCollapseStart .MatchWildcards = False .Text = "" End With End Sub

Sub ClearFindAndReplaceParameters() With Selection.Find .ClearFormatting .Replacement.ClearFormatting

Ni pht hnh: www.giaiphapexcel.com

140

L p trnh VBA for Word


.Text = "" .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = False .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False End With End Sub

6.3.1. Notes regarding Workaround 2


Unfortunately, this fix requires you to display the Find and Replace dialog momentarily; and there is no way around this. It only flashes up for an instant and is gone, but Workaround 1 doesn't suffer from this drawback, and also has the major adavantage that you don't need to run a macro every time you do a wildcard Find & Replace! To get the ID of any CommandBar control, you can use code such as the following:
MsgBox CommandBars("Menu Bar"). Controls("&Edit").Controls("R&eplace...").ID

Executing a CommandBar control using the FindControl method:


CommandBars.FindControl(ID:=313).Execute

... has two advantages over referring to it by name as in:


CommandBars("Menu Bar").Controls("&Edit").Controls("R&eplace...").Execute

1. If the user has customized Normal.dot (renamed or moved the control), your code will still work 2. It is language-independent Note for advanced users: Unfortunately, you cannot run the FixWildcardBug macro using the Application.Run method. If you do, the SendKeys statement doesn't work properly, and the Edit + Replace dialog remains open until the user cancels it. So if you want to store the FixWildcardBug macro in an add-in and call it from a different project, you have to set a reference to the add-in in your other project and call your macro, rather than using Application.Run. Similarly, if you have an add-in containing another procedure that, in turn, calls the FixWildcardBug macro, then you can't run that other procedure from a different project using Application.Run either; instead, you have to set a reference to the add-in and call the other procedure.. Apart from the problem with Application.Run, in English versions of Word this fix works reliably in all situations (as far as we can tell from extensive tests); but apparently, in some other language versions of Word it may not work reliably if you assign your macro to Alt+Ctrl+Anything. So if using non-English versions of Word and if assigning it to a keyboard
Ni pht hnh: www.giaiphapexcel.com

141

L p trnh VBA for Word shortcut, avoid assigning it to Alt+Ctrl. Or just use Method 1!

7. How to find out, using VBA, how many replacements Word made during a Find & Replace All
Or: How to find out how many occurrences there are of a particular word in a document Article contributed by Bart Verbeek and Dave Rado When you click Replace All in the Find & Replace dialog, Word shows the number of replacements on the Status bar after the operation is completed. To the regret of many it is impossible to query this number in VBA. But that does not mean you cannot determine the number of replacements if you want to. The following VBA code sample does just that. (If you are not familiar with using functions with arguments, see How to cut out repetition and write much less code, by using subroutines and functions that take arguments).
Function CountNoOfReplaces(StrFind As String, StrReplace As String) Dim NumCharsBefore As Long, NumCharsAfter As Long, LengthsAreEqual As Boolean Application.ScreenUpdating = False 'Check whether the length of the Find and Replace strings are the same; _ if they are, prefix the replace string with a hash (#) If Len(StrFind) = Len(StrReplace) Then LengthsAreEqual = True StrReplace = "#" & StrReplace End If 'Get the number of chars in the doc BEFORE doing Find & Replace NumCharsBefore = ActiveDocument.Characters.Count 'Do the Find and Replace With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = StrFind .Replacement.Text = StrReplace .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = True .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False .Execute Replace:=wdReplaceAll Ni pht hnh: www.giaiphapexcel.com

142

L p trnh VBA for Word


End With 'Get the number of chars AFTER doing Find & Replace NumCharsAfter = ActiveDocument.Characters.Count 'Calculate of the number of replacements, 'and put the result into the function name variable CountNoOfReplaces = (NumCharsBefore - NumCharsAfter) / _ (Len(StrFind) - Len(StrReplace)) 'If the lengths of the find & replace strings were equal at the start,do another replace to strip out the # If LengthsAreEqual Then StrFind = StrReplace 'Strip off the hash StrReplace = Mid$(StrReplace, 2) With Selection.Find .Text = StrFind .Replacement.Text = StrReplace .Execute Replace:=wdReplaceAll End With End If Application.ScreenUpdating = True 'Free up memory ActiveDocument.UndoClear End Function

You could call it like this:


Sub Test() MsgBox "Number of replacements: " & CountNoOfReplaces _ (StrFind:="Big", StrReplace:="Bigger"), vbInformation End Sub

This will work regardless of which string is the longest, and even if the strings do not differ in length. You could take the same principle further, to count the number of occurrences of a particular word in a document:
Function CountWord(WordToCount As String) Dim NumCharsBefore As Long, NumCharsAfter As Long Application.ScreenUpdating = False 'Get the number of chars in the doc BEFORE doing Find & Replace Ni pht hnh: www.giaiphapexcel.com

143

L p trnh VBA for Word


NumCharsBefore = ActiveDocument.Characters.Count 'Do the Find and Replace With Selection.Find .ClearFormatting .Replacement.ClearFormatting .Text = WordToCount .Replacement.Text = "#" & WordToCount .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = True .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False .Execute Replace:=wdReplaceAll End With 'Get the number of chars AFTER doing Find & Replace NumCharsAfter = ActiveDocument.Characters.Count 'Calculate of the number of replacements, 'and put the result into the function name variable CountWord = NumCharsAfter - NumCharsBefore 'Undo the replace ActiveDocument.Undo 'Free up memory ActiveDocument.UndoClear Application.ScreenUpdating = False End Function

You could call it like this:


Sub Test() Dim Response As String Response = InputBox("Type a word you want to count", _ "Get number of occurrences of this word") MsgBox "There are " & CountWord(Response) & _ " occurrences of the word '" & Response & _ "' in this document", vbInformation End Sub

Ni pht hnh: www.giaiphapexcel.com

144

L p trnh VBA for Word Note: although it is possible to achieve the same ends by using a counter while you do multiple Finds (or multiple Find & Replaces) one at a time, until nothing more is found, that method is much slower than doing a ReplaceAll as illustrated above.

8. Finding out how many times some text appears in a document


Article contributed by Jonathan West You can find out how often a particular phrase appears in a document with a fairly simple VBA routine. Paste the following code into the VBA editor and run it.
Public Sub CountOccurrences() Dim iCount As Long Dim strSearch As String strSearch = InputBox$("Type in the text you want to search for.") iCount = 0 With ActiveDocument.Content.Find .Text = strSearch .Format = False .Wrap = wdFindStop Do While .Execute iCount = iCount + 1 Loop End With MsgBox Chr$(34) & strSearch & Chr$(34) & " was found " & _ iCount & " times." End Sub

9. How to use Edit Find to select everything from where the cursor is to the first found item
Article contributed by Ibby Turn on Extend mode, do the Find, and turn Extend mode off again. If doing this manually, you can turn Extend mode on by pressing F8, and you can turn it off by pressing Esc. Or you can double-click in the status bar, where it says EXT, both to switch it on and to switch it off again (it will be greyed out unless Extend mode is on).
Ni pht hnh: www.giaiphapexcel.com

145

L p trnh VBA for Word As well as text you can use this to find and select to a particular Style, or a special character, or whatever you like.

9.1. Doing it programmatically


If doing it programmatically, use something like the following:
Application.ScreenUpdating = False ' Turn on ExtendMode Selection.ExtendMode = True ' Perform the search With Selection.Find .ClearFormatting .Text = "fox" .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = False .MatchWholeWord = True .MatchWildcards = False .MatchSoundsLike = False .MatchAllWordForms = False .Execute If .Found Then MsgBox "The selection has been extended." Else MsgBox "Search word not found." End If End With ' Turn off ExtendMode Selection.ExtendMode = False Application.ScreenUpdating = True

Warning: If doing it programmatically, and if you plan to follow up by doing further Find operations within the resulting selection, stick to using Selection.Find. Otherwise, timing problems sometimes seem to result. Or alternatively, use Range.Find throughout and don't use ExtendMode at all, as in the following example:
Dim MyRange As Range, StartRange As Range Set MyRange = Selection.Range MyRange.Collapse wdCollapseStart Set StartRange = MyRange.Duplicate With MyRange.Find .Forward = True .Wrap = wdFindStop Ni pht hnh: www.giaiphapexcel.com

146

L p trnh VBA for Word


.Text = "fox" .Replacement.Text = "" .Execute If .Found Then ''Extend the range from the found item back to the start of the original range MyRange.Start = StartRange.Start Set StartRange = MyRange.Duplicate ' Within myRange, change some text. With MyRange.Find .Forward = True .Wrap = wdFindStop .Text = "brown" .Replacement.Text = "red" .Execute Replace:=wdReplaceAll End With End If End With

If using Word 97, Selection.Find is much faster than Range.Find (this bug was fixed in Word 2000); so if any of your users are using Word 97, the former method (using ExtendMode and sticking to Selection.Find throughout) is probably the best idea.

10. How to replace text in quotation marks with italic or highlighted text minus the quotes
Article contributed by Klaus Linke and Dave Rado This is easy, using a wildcard search and replace.

10.1. To do it manually
1. First make sure Replace straight quotes with Smart quotes is ticked, on the AutoFormat As You Type tab under Tools + Autocorrect; and if you want to use a highlight in the replace, make sure the default highlight colour on the Reviewing toolbar is set to the colour you want. 2. Then replace " with " (all quotation marks with themselves), which will replace any straight quotes in the document with smart quotes. 3. Now do your wildcard Find and Replace. Your Find What text needs to be:
()(*)()

Note that the quotes must be smart quotes: you can paste them from the document into the Find what: box.
Ni pht hnh: www.giaiphapexcel.com

147

L p trnh VBA for Word Your Replace with text needs to be:
\2

And in the Replace with: box, either select Format + Font + Italic, or Format + Highlight, depending on your preference. 4. Finally, remove any orpaned opening smart quotes (as you might have if you'd had a quotation spanning more than one paragraph), by replacing the opening quote character (which, as before, you can cut and paste into the dialog from the document; don't type it) with nothing.

10.2. To do it with a macro


Either use:
Sub ReplaceQuotesWithHighlight() Dim SmartQtSetting As Boolean, DefHighlight As Long 'Make sure smartquotes are turned on SmartQtSetting = Options.AutoFormatAsYouTypeReplaceQuotes Options.AutoFormatAsYouTypeReplaceQuotes = True 'Make sure highlight is set to the colour you want, e.g.: DefHighlight = Options.DefaultHighlightColorIndex Options.DefaultHighlightColorIndex = wdGreen With Selection.Find 'Set parameters .ClearFormatting .Replacement.ClearFormatting .Forward = True .Wrap = wdFindContinue .MatchCase = False .MatchWholeWord = False .MatchAllWordForms = False .MatchSoundsLike = False 'First do a replace to make sure quotes are all smartquotes .Format = False .MatchWildcards = False .Text = """" .Replacement.Text = """" .Execute Replace:=wdReplaceAll 'Then do the wilcard replace .Replacement.Highlight = True .Format = True .Wrap = wdFindContinue .MatchWildcards = True 'ChrW(8220) is the open quote character and ChrW(8221) is the close Ni pht hnh: www.giaiphapexcel.com

148

L p trnh VBA for Word


quote .Text = "(" & ChrW(8220) & ")(*)(" & ChrW(8221) & ")" .Replacement.Text = "\2" .Execute Replace:=wdReplaceAll 'Then remove any orphaned opening quotes .Format = False .Wrap = wdFindContinue .MatchWildcards = False .Text =ChrW(8220) .Replacement.Text = "" .Execute Replace:=wdReplaceAll 'Clear dialog of all non-default settings .Text = "" .Execute End With 'Reset options to the way they were Options.AutoFormatAsYouTypeReplaceQuotes = SmartQtSetting Options.DefaultHighlightColorIndex = DefHighlight End Sub

Or use:
Sub ReplaceQuotesWithItalic() Dim SmartQtSetting As Boolean 'Make sure smartquotes are turned on SmartQtSetting = Options.AutoFormatAsYouTypeReplaceQuotes Options.AutoFormatAsYouTypeReplaceQuotes = True With Selection.Find 'Set parameters .ClearFormatting .Replacement.ClearFormatting .Forward = True .Wrap = wdFindContinue .MatchCase = False .MatchWholeWord = False .MatchAllWordForms = False .MatchSoundsLike = False 'First do a replace to make sure quotes are all smartquotes .Format = False .MatchWildcards = False .Text = """" .Replacement.Text = """" .Execute Replace:=wdReplaceAll Ni pht hnh: www.giaiphapexcel.com

149

L p trnh VBA for Word


'Then do the wilcard replace .Replacement.Font.Italic = True .Format = True .Wrap = wdFindContinue .MatchWildcards = True 'ChrW(8220) is the open quote character and ChrW(8221) is the close quote .Text = "(" & ChrW(8220) & ")(*)(" & ChrW(8221) & ")" .Replacement.Text = "\2" .Execute Replace:=wdReplaceAll 'Then remove any orphaned opening quotes .Format = False .Wrap = wdFindContinue .MatchWildcards = False .Text = ChrW(8220) .Replacement.Text = "" .Execute Replace:=wdReplaceAll 'Clear dialog of all non-default settings .Text = "" .Execute End With 'Reset options to the way they were Options.AutoFormatAsYouTypeReplaceQuotes = SmartQtSetting End Sub

11. Replace each instance of the text string Document One with the contents of a file called c:\test\Doc1.doc
Article contributed by Bill Coan
Sub ReplaceTagWithFile() Selection.HomeKey unit:=wdStory With Selection.Find .ClearFormatting .Text = "Document One" .Replacement.Text = "" .Forward = True .Wrap = wdFindStop .Format = False .MatchCase = True .MatchWholeWord = False .MatchWildcards = False Ni pht hnh: www.giaiphapexcel.com

150

L p trnh VBA for Word


.MatchSoundsLike = False .MatchAllWordForms = False Do While .Execute() Selection.InsertFile _ FileName:="c:\test\Doc1.doc", Range:="", _ ConfirmConversions:=False, _ Link:=False, _ Attachment:=False Loop End With End Sub

12. Replace one character with another wherever it appears in a string


Article contributed by Bill Coan
' the following code calls the "ReplaceACharacter" function Sub TestTheFunction() Dim myString As String Dim OldCharacter As String Dim NewCharacter As String myString = "Hello, I love you. Let me jump in your game." OldCharacter = "e" NewCharacter = "u" myString = ReplaceACharacter(InWhat:=myString, FindWhat:=OldCharacter, _ ReplaceWith:=NewCharacter) MsgBox myString End Sub 'here's the function itself Function ReplaceACharacter(InWhat As String, FindWhat As String, ReplaceWith As String) As String Dim StartAtCharacter As Long StartAtCharacter = 1 StartAtCharacter = InStr(StartAtCharacter, InWhat, FindWhat) Do While StartAtCharacter <> 0 Ni pht hnh: www.giaiphapexcel.com

151

L p trnh VBA for Word


InWhat = Left$(InWhat, StartAtCharacter - 1) _ & ReplaceWith _ & Mid$(InWhat, StartAtCharacter + 1) StartAtCharacter = InStr(StartAtCharacter, InWhat, FindWhat) Loop ReplaceACharacter = InWhat End Function

13. Remove the underline attribute from characters with descenders


Article contributed by Bill Coan Select the text that you want to fix, then run the following macro
Sub FixUnderlinedText() For Each oCharacter In Selection.Characters If oCharacter Like "[qypjg]" Then oCharacter.Font.Underline = wdUnderlineNone End If Next oCharacter End Sub

14. Remove all empty paragraphs from a document


Article contributed by Dave Rado You can remove most empty paragraphs from a document by doing a wildcard Find & Replace. Replace: ^13{2,} with ^p, which (in theory see below) replaces all occurrences of two or more consecutive paragraph marks with one paragraph mark. Or you can run the following macro, which does the same thing:
With Selection.Find .Text = "^13{2,}" .Replacement.Text = "^p" .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = True Ni pht hnh: www.giaiphapexcel.com

152

L p trnh VBA for Word


.Execute Replace:=wdReplaceAll End With

(Note that using Find and Replace is dramatically faster than cycling through the Paragraphs collection). However, you can't use Find & Replace to delete the first or last paragraph in the document, if they are empty. To delete them you would need to add the following code to the above macro:
Dim MyRange As Range Set MyRange = ActiveDocument.Paragraphs(1).Range If MyRange.Text = vbCr Then MyRange.Delete Set MyRange = ActiveDocument.Paragraphs.Last.Range If MyRange.Text = vbCr Then MyRange.Delete

In addition, you can't use Find & Replace to delete the paragraph immediately preceding or following any tables, if these are empty. You would need to add the following code to the macro if you want them deleted but be careful; if two tables are separated only by an empty paragraph, the following code will merge them into one table, which may or may not be the result you wanted:1
Dim oTable As Table, MyRange As Range For Each oTable In ActiveDocument.Tables #If VBA6 Then 'The following is only compiled and run if Word 2000 or 2002 is in use 'It speeds up the table and your code oTable.AllowAutoFit = False #End If 'Set a range to the para following the current table Set MyRange = oTable.Range MyRange.Collapse wdCollapseEnd 'if para after table empty, delete it If MyRange.Paragraphs(1).Range.Text = vbCr Then MyRange.Paragraphs(1).Range.Delete End If 'Set a range to the para preceding the current table Set MyRange = oTable.Range MyRange.Collapse wdCollapseStart MyRange.Move wdParagraph, -1 'if para before table empty, delete it If MyRange.Paragraphs(1).Range.Text = vbCr Then MyRange.Paragraphs(1).Range.Delete End If Next oTable

You also can't use Find & Replace to delete the first or last paragraph in a table cell, if empty. If the user inserted an empty paragraph at the start or end of a table cell (in order to simulate space before 153 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word paragraph or space after paragraph), you have to use something like the following to remove those empty paragraphs:
Dim oTable As Table, oCell As Cell, MyRange As Range For Each oTable In ActiveDocument.Tables 'Using oCell.Next to cycle through table cells is much quicker ' in long tables than using For Each oCell Set oCell = oTable.Range.Cells(1) For Counter = 1 To oTable.Range.Cells.Count If Len(oCell.Range.Text) > 2 And _ oCell.Range.Characters(1).Text = vbCr Then 'if cell is NOT blank, but it starts with a blank paragraph, delete the blank para 'Note that a blank cell contains 2 characters; 'a paragraph mark and an end of cell marker oCell.Range.Characters(1).Delete End If If Len(oCell.Range.Text) > 2 And _ Asc(Right$(oCell.Range.Text, 3)) = 13 Then 'if cell is NOT blank, but it ends with a blank paragraph, delete the blank para Set MyRange = oCell.Range MyRange.MoveEnd Unit:=wdCharacter, Count:=-1 MyRange.Characters.Last.Delete End If Set oCell = oCell.Next Next Counter Next oTable

So the complete macro would look like this:


Sub DeleteEmptyParas() Dim MyRange As Range, oTable As Table, oCell As Cell With Selection.Find .Text = "^13{2,}" .Replacement.Text = "^p" .Forward = True .Wrap = wdFindContinue .Format = False .MatchCase = False .MatchWholeWord = False .MatchAllWordForms = False .MatchSoundsLike = False .MatchWildcards = True .Execute Replace:=wdReplaceAll End With Ni pht hnh: www.giaiphapexcel.com

154

L p trnh VBA for Word


Set MyRange = ActiveDocument.Paragraphs(1).Range If MyRange.Text = vbCr Then MyRange.Delete Set MyRange = ActiveDocument.Paragraphs.Last.Range If MyRange.Text = vbCr Then MyRange.Delete For Each oTable In ActiveDocument.Tables #If VBA6 Then 'The following is only compiled and run if Word 2000 or 2002 is in use 'It speeds up the table and your code oTable.AllowAutoFit = False #End If 'Set a range to the para following the current table Set MyRange = oTable.Range MyRange.Collapse wdCollapseEnd 'if para after table empty, delete it If MyRange.Paragraphs(1).Range.Text = vbCr Then MyRange.Paragraphs(1).Range.Delete End If 'Set a range to the para preceding the current table Set MyRange = oTable.Range MyRange.Collapse wdCollapseStart MyRange.Move wdParagraph, -1 'if para before table empty, delete it If MyRange.Paragraphs(1).Range.Text = vbCr Then MyRange.Paragraphs(1).Range.Delete End If Next oTable End Sub

1. You could modify the macro to cater for that; for example, if my formatting macro finds a blank paragraph separating two tables, it applies the Heading 1 style to that paragraph and inserts the text: Heading text needs to go here at that point; and at the end of the macro, a message box is displayed (when appropriate) warning the user that they need to type meaningful heading text at those places, and explaining how to find them. However, the code to do that is beyond the scope of this article.

Ni pht hnh: www.giaiphapexcel.com

155

L p trnh VBA for Word

CHAPTER 10: Working with files and directories

1. How to allow the user to browse to and select a folder


Article contributed by Jonathan West There are three options available. 1. Use the Copy File dialog:
With Dialogs(wdDialogCopyFile) If .Display <> 0 Then MsgBox "You chose " & .Directory Else MsgBox "Dialog cancelled" End If End With

The advantage is that it is quick & simple, but the disadvantage is that you can't change the caption of the dialog, which says Copy. 2. Go to www.mvps.org/ccrp/ and download their BrowseDialog Server component (it's free) and use that. It gives you a fully configurable browse for folders dialog which you treat in much the same way as you treat a class. It a bit more complex to use, but looks much more professional. 3. Go to http://www.mvps.org/btmtz/browsdlg/ and pick up the BrowseDlg code by Brad Martinez. This gives you the same capability as the BrowseDialog Server, except that you can't have a New folder button on the dialog. The advantage is that you don't have to distribute an additional DLL with your template, everything is included within your code. 4. If you are using Word 2002 (Office XP) or later, you can use the built-in FileDialog object. With Application.FileDialog(msoFileDialogFolderPicker) .AllowMultiSelect = False If .Show <> 0 Then MsgBox "You chose " & .SelectedItems(1) Else MsgBox "Dialog cancelled" End If
Ni pht hnh: www.giaiphapexcel.com

156

L p trnh VBA for Word End With


The above is a simple code sample, you can do more in terms of setting the dialog caption and the starting folder. Look up the details in the VBA Help.

2. How to Find & ReplaceAll on a batch of documents in the same folder


Article contributed by Ibby The following code, if stored in a Global template, will perform a Find & ReplaceAll in all of the documents in a specified folder. The FindReplace dialog is displayed for the first document only. The user sets the parameters in the dialog and presses Replace All and then Close. The user is then asked whether to process all of the files in the specified directory if Yes, the rest of the files are processed with the settings as entered in the original FindReplace dialog.
Option Explicit Public Sub BatchReplaceAll() Dim Dim Dim Dim Dim FirstLoop As Boolean myFile As String PathToUse As String myDoc As Document Response As Long

PathToUse = "C:\Test\" 'Error handler to handle error generated whenever 'the FindReplace dialog is closed On Error Resume Next 'Close all open documents before beginning Documents.Close SaveChanges:=wdPromptToSaveChanges 'Boolean expression to test whether first loop 'This is used so that the FindReplace dialog will 'only be displayed for the first document FirstLoop = True 'Set the directory and type of file to batch process myFile = Dir$(PathToUse & "*.doc") Ni pht hnh: www.giaiphapexcel.com

157

L p trnh VBA for Word


While myFile <> "" 'Open document Set myDoc = Documents.Open(PathToUse & myFile) If FirstLoop Then 'Display dialog on first loop only Dialogs(wdDialogEditReplace).Show FirstLoop = False Response = MsgBox("Do you want to process " & _ "the rest of the files in this folder", vbYesNo) If Response = vbNo Then Exit Sub Else 'On subsequent loops (files), a ReplaceAll is 'executed with the original settings and without 'displaying the dialog box again With Dialogs(wdDialogEditReplace) .ReplaceAll = 1 .Execute End With End If 'Close the modified document after saving changes myDoc.Close SaveChanges:=wdSaveChanges 'Next file in folder myFile = Dir$() Wend End Sub

If you want to perform the Find & ReplaceAll in all subfolders as well, use the FileSearch object instead of Dir. Dir is significantly faster than FileSearch when searching a single directory and it's also simpler to code; but when searching all subdirectories as well, it's simplest to use FileSearch, e.g.:
Option Explicit Public Sub BatchReplaceAll() Dim FirstLoop As Boolean Dim myFile As String Ni pht hnh: www.giaiphapexcel.com

158

L p trnh VBA for Word


Dim Dim Dim Dim PathToUse As String myDoc As Document Response As Long i As Long

PathToUse = "C:\Test\" 'Error handler to handle error generated whenever 'the FindReplace dialog is closed On Error Resume Next 'Close all open documents before beginning Documents.Close SaveChanges:=wdPromptToSaveChanges 'Boolean expression to test whether first loop 'This is used so that the FindReplace dialog will 'only be displayed for the first document FirstLoop = True 'Set the directory and type of file to batch process With Application.FileSearch .NewSearch .LookIn = PathToUse .SearchSubFolders = True .FileName = "*.doc" .MatchTextExactly = True .FileType = msoFileTypeAllFiles If .Execute() Then For i = 1 To .FoundFiles.Count 'Open document Set myDoc = Documents.Open(.FoundFiles(i)) If FirstLoop Then 'display dialog on first loop only Dialogs(wdDialogEditReplace).Show FirstLoop = False Response = MsgBox("Do you want to process " & _ "the rest of the files in this folder", vbYesNo) If Response = vbNo Then Exit Sub Else 'On subsequent loops (files), a ReplaceAll is Ni pht hnh: www.giaiphapexcel.com

159

L p trnh VBA for Word


'executed with the original settings and without 'displaying the dialog box again With Dialogs(wdDialogEditReplace) .ReplaceAll = 1 .Execute End With End If 'Close the modified document after saving changes myDoc.Close SaveChanges:=wdSaveChanges Next i End If End With End Sub

3. Skipping Password-Protected Documents in a Batch Process


Article contributed by Ibby The Documents.Open method has a PasswordDocument parameter where you can specify the password of the document to be opened. If you put in a nonsense password (i.e.: one that is unlikely to be used by any sane person as a password!), you will observe the following behaviour: If the document has no password assigned, the Documents.Open method ignores the PasswordDocument parameter and the document is opened normally. If the document has a password, the password specified in the PasswordDocument will be wrong, generating an error 5408 "The password is incorrect....". You can trap this error and loop back to the next file to be opened. The following is an example of how to trap this error.
Option Explicit Public Sub ProcessBatch() Dim strFileName As String Dim strFilePath As String Dim oDoc As Document ' Set Directory for Batch Process strFilePath = "C:\Test\" ' Get Name of First .doc File from Directory strFileName = Dir$(strFilePath & "*.doc")

Ni pht hnh: www.giaiphapexcel.com

160

L p trnh VBA for Word


While Len(strFileName) <> 0 ' Set Error Handler On Error Resume Next ' Attempt to Open the Document Set oDoc = Documents.Open( _ FileName:=strFilePath & strFileName, _ PasswordDocument:="?#nonsense@$") Select Case Err.Number Case 0 ' Document was Successfully Opened Debug.Print strFileName & " was processed." Case 5408 ' Document is Password-protected and was NOT Opened Debug.Print strFileName & " is password-protected " & _ "and was NOT processed." ' Clear Error Object and Disable Error Handler Err.Clear On Error GoTo 0 ' Get Next Document GoTo GetNextDoc Case Else ' Another Error Occurred MsgBox Err.Number & ":" & Err.Description End Select ' Disable Error Handler On Error GoTo 0 '------------------------------------'------------------------------------'---Perform Action on Document Here--'------------------------------------'------------------------------------' Close Document oDoc.Close ' Clear Object Variable Set oDoc = Nothing GetNextDoc: ' Get Next Document from Specified Directory strFileName = Dir$() Wend End Sub Ni pht hnh: www.giaiphapexcel.com

161

L p trnh VBA for Word

4. How to check if a file has already been opened by another user


Article contributed by Jonathan West The following function will return True if a file is already in use by another user, and False if it is available for use. The function is useful if you are running a macro on a set of files, and want to avoid having it stop with the dialog asking if you want to open a copy of the file
Function FileLocked(strFileName As String) As Boolean On Error Resume Next ' If the file is already opened by another process, ' and the specified type of access is not allowed, ' the Open operation fails and an error occurs. Open strFileName For Binary Access Read Lock Read As #1 Close #1 ' If an error occurs, the document is currently open. If Err.Number <> 0 Then FileLocked = True Err.Clear End If End Function

You could call it like this:


Sub Test() If Not FileLocked("C:\Temp\Temp.doc") Then Documents.Open "C:\Temp\Temp.doc" End If End Sub

5. How to copy an open file using VBA


Article contributed by Dave Rado The VBA FileCopy statement will not copy files that are open. However, the WordBasic equivalent will (this is what is known as progress!).
Ni pht hnh: www.giaiphapexcel.com

162

L p trnh VBA for Word Unfortunately, the syntax of WordBasic equivalent is different in Word 97 and Word 2000! The following works even if the file being copied is open:
If Left$(Application.Version, 1) = "8" Then 'Word 97 WordBasic.CopyFile FileName:="c:\OldDirectory\Temp.doc", _ Directory:="C:\NewDirectory\Temp.doc" Else 'Word 2000 and above WordBasic.CopyFileA FileName:="c:\OldDirectory\Temp.doc", _ Directory:="C:\NewDirectory\Temp.doc" End If

Why did they add the extra A in Word 2000? Why does it always rain at weekends but never during the week? Microsoft works in mysterious ways, their wonders to perform. Maybe one of the developers had a hangover the day that change was made.

6. How to create a copy of an open document


Article contributed by Ibby If your current document has been saved at least once, the following will: create a new document that is an exact copy of the current document it will include any modifications that have not yet been saved. prompt you for a location and filename to save this copy (you can hardcode the path and filename if you like) close this copy All this occurs without saving the current document.
Dim myCopy As Document Dim docName As String ' Retrieve name of ActiveDocument docName = ActiveDocument.Name ' Test if Activedocument has previously been saved If ActiveDocument.Path = "" Then ' If not previously saved MsgBox "The current document must be saves at least once." Else ' If previously saved, create a copy Set myCopy = Documents.Add(ActiveDocument.FullName) Ni pht hnh: www.giaiphapexcel.com

163

L p trnh VBA for Word


' Show SaveAs dialog to allow user to save copy With Dialogs(wdDialogFileSaveAs) ' Set name in SaveAs dialog .Name = "Copy_of_" & docName .Show End With ' Close copy myCopy.Close End If

7. How to save a document using a filename that gets incremented by 1 each time if the filename already exists
Article contributed by Dave Rado Use:
Sub SaveIncrementedFilename() Dim PathAndFileName As String, n As Long PathAndFileName = "C:\Data\temp" If Dir(PathAndFileName & ".doc") = "" Then ActiveDocument.SaveAs (PathAndFileName & ".doc") Else n = 1 Do While Dir(PathAndFileName & n & ".doc") <> "" n = n + 1 Loop ActiveDocument.SaveAs PathAndFileName & n & ".doc" End If End Sub

8. Insert into a document the names of all files in a selected folder


Article contributed by Bill Coan
Sub InsertNamesOfFilesInAFolder() Dim MyPath As String Dim MyName As String

Ni pht hnh: www.giaiphapexcel.com

164

L p trnh VBA for Word


'let user select a path With Dialogs(wdDialogCopyFile) If .Display() <> -1 Then Exit Sub MyPath = .Directory End With 'strip quotation marks from path If Len(MyPath) = 0 Then Exit Sub If Asc(MyPath) = 34 Then MyPath = Mid$(MyPath, 2, Len(MyPath) - 2) End If 'get files from the selected path 'and insert them into the doc MyName = Dir$(MyPath & "*.*") Do While MyName <> "" Selection.InsertAfter MyName & vbCr MyName = Dir Loop 'collapse the selection Selection.Collapse wdCollapseEnd End Sub

9. How to retrieve Word's default Documents path or Pictures path setting


Article contributed by Bill Coan You cannot use:
myDocPath = Options.DefaultFilePath (wdDocumentsPath)

and Dave Rado

to get the default document path, because this returns the current FileOpen path, not the default documents path!! Instead, use:
Dim myDocPath As String myDocPath = Dialogs(wdDialogToolsOptionsFileLocations).Setting 'Add a "\" at the end of the path, unless the setting is already followed by a "\" 'which it will be if the setting is set to a root folder If Not Right$(myDocPath, 1) = "\" Then myDocPath = myDocPath + "\" Ni pht hnh: www.giaiphapexcel.com

165

L p trnh VBA for Word


End If MsgBox myDocPath

Similarly, you cannot use:


myPicPath = Options.DefaultFilePath(wdPicturesPath)

... because if the user has inserted a picture from a different folder, the Default File Path returns that folder rather than the actual setting from the dialog box. Instead, you can use:
With Dialogs(wdDialogToolsOptionsFileLocations) .Path = "PICTURE-PATH" .Update myPicPath = .Setting If Not Right$(myPicPath, 1) = "\" Then myPicPath = myPicPath + "\" End If MsgBox myPicPath End With

10. How to delete files using VBA, including files which may be readonly
Article contributed by Dave Rado and Doug Steele You can use VBA Kill statement to delete files, as in:
Kill path & filename

However, the Kill statement can't delete readonly files, so, unless there's no chance that the file could be marked as readonly, you must first remove the readonly attribute from the file. You can do this as follows:
Dim KillFile As String KillFile = "c:\temp\temp.doc" 'Check that file exists If Len(Dir$(KillFile)) > 0 Then 'First remove readonly attribute, if set SetAttr KillFile, vbNormal 'Then delete the file Kill KillFile End If

Ni pht hnh: www.giaiphapexcel.com

166

L p trnh VBA for Word That's a bit unwieldy, so it's better to create a public macro that takes the path & filename as an argument, which you can then call whenever you like from other macros, as follows:
Public Sub KillProperly(Killfile As String) If Len(Dir$(Killfile)) > 0 Then SetAttr KillFile, vbNormal Kill KillFile End If End Sub

You could then call it from another macro like this:


KillProperly "c:\temp\temp.doc"

To delete all the files in a given directory:


'Loop through all the files in the directory by using Dir$ function Dim MyFile As String MyFile = Dir$("c:\temp\*.*") Do While MyFile <> "" KillProperly "c:\temp\" & MyFile 'need to specify full path again because a file was deleted 1 MyFile = Dir$("c:\temp\*.*") Loop

To delete all the Word documents in a given directory, leaving other files alone:
Dim MyFile As String MyFile = Dir$("c:\temp\*.doc") Do While MyFile <> "" KillProperly "c:\temp\" & MyFile MyFile = Dir$("c:\temp\*.doc") Loop

__________________
1. Alternatively you could read all the values into an array, and then delete all the files using a second loop. That would run slightly quicker, but unless it's a very large directory indeed, you wouldn't notice any difference.

11. How to read the filenames of all the files in a directory into an array
For instance, in order to populate a list box Article contributed by Dave Rado You can use the Dir$ function to do this. (Dir and Dir$ are functionally identical, but Dir$ runs marginally faster). Dir$ is a lot faster than using FileSearch, unless many subdirectories need to be searched or you need to use the advanced features of FileSearch such as the LastModified property.
Ni pht hnh: www.giaiphapexcel.com

167

L p trnh VBA for Word


Dim MyFile As String Dim Counter As Long 'Create a dynamic array variable, and then declare its initial size Dim DirectoryListArray() As String ReDim DirectoryListArray(1000) 'Loop through all the files in the directory by using Dir$ function MyFile = Dir$("c:\temp\*.*") Do While MyFile <> "" DirectoryListArray(Counter) = MyFile MyFile = Dir$ Counter = Counter + 1 Loop 'Reset the size of the array without losing its values by using Redim Preserve Redim Preserve DirectoryListArray(Counter - 1)

To prove it worked you could run the following:


For Counter = 0 To UBound(DirectoryListArray) 'Debug.Print writes the results to the Immediate window (press Ctrl + G to view it)' Debug.Print DirectoryListArray(Counter) Next Counter

To populate a Listbox from the array you could use:


ListBox1.List = DirectoryListArray

12. How to get the names of all the folders in the folder tree, starting from a specified folder
For instance, in order put the results into a document, or to print them Article contributed by Thomas Gahler The following procedures read the entire folder tree, starting from any folder you specify, into an array, and put the results into a document; or you could have it print the results, or whatever you want it to do with them. Copy the functions below into a module in your VB Editor (press Alt+F11). You will need to delete the horizontal lines that have been used here to separate the functions.
Ni pht hnh: www.giaiphapexcel.com

168

L p trnh VBA for Word You can call the functions with the following macro (or modify it as required)):
Sub Demo() Dim FoldersArray As Variant Dim i As Integer 'Read all subfolders of the specified folder into an array 'by calling the funcGetSubfolders function FoldersArray = funcGetSubfolders("C:\Windows") 'Put the results (the array values) into the current document if it is blank, 'or else into a new document If Len(ActiveDocument.Range.Text) > 1 Then Documents.Add End If For i = LBound(FoldersArray) To UBound(FoldersArray) ActiveDocument.Range.InsertAfter FoldersArray(i) & vbCr Next i ActiveDocument.Saved = True End Sub

Or you could allow the user to type in a path for themselves, by displaying an input box or a UserForm. For instance, instead of the line:
FoldersArray = funcGetSubfolders("C:\Windows")

you could use:


Dim FolderToRead As String Do FolderToRead = InputBox("Type path to folder you want to check") If Len(FolderToRead) = 0 Then Exit Sub End If If Len(Dir$(FolderToRead, vbDirectory)) = 0 Then MsgBox "Invalid folder name; please try again, or press Cancel to quit" End If Loop Until Len(Dir$(FolderToRead, vbDirectory)) > 0 FoldersArray = funcGetSubfolders(FolderToRead)

Or see How to allow the user to browse to and select a folder, if you want to be really swish. These are the functions you need. See also the Notes at the end.
Public Function funcGetSubfolders(ByVal FolderToRead As String) As Variant Ni pht hnh: www.giaiphapexcel.com

169

L p trnh VBA for Word


'This function uses a string as a parameter and not an array. 'It translates this string to an array and then starts the main function, 'funcGetAllSubfolders' Dim AllSubFolders(0) As Variant On Error Resume Next System.Cursor = wdCursorWait 'Add a backslash to the end of the path, if not there already If (Right$(FolderToRead, 1) <> "\") Then FolderToRead = FolderToRead & "\" End If 'Set the path as the first entry in the array and pas the array to the main function AllSubFolders(0) = FolderToRead funcGetSubfolders = funcGetAllSubfolders(AllSubFolders) System.Cursor = wdCursorNormal StatusBar = "" On Error GoTo 0 End Function

Private Function funcGetAllSubfolders(ByVal AllSubFoldersArray As Variant) As Variant 'This is a recursive function, that is, it keeps calling itself 'which makes it a nightmare to step through! Dim Counter As Integer 'The following string will contain the path of the folder which is currently being looked in Dim CurFolderName As String 'The following string will contain the current value returned by Dir$(). Dim SubFolderName As String 'The following array will contain of the subfolders (if any) of 'CurFolderName' Dim SubFolderList() As String On Error Resume Next 'Get the last value we put into the AllSubFoldersArray Array variant, 'and convert it to a string so that we can assign it to the string 'variable CurFolderName CurFolderName = CStr(AllSubFoldersArray(UBound(AllSubFoldersArray)))

Ni pht hnh: www.giaiphapexcel.com

170

L p trnh VBA for Word


'Read all subfolders of 'CurFolderName' and add them to 'SubFolderList'. ReDim SubFolderList(0) SubFolderName = Dir$(CurFolderName, vbDirectory) Do While Len(SubFolderName) <> 0 'Ignore the current directory and the encompassing directory. If SubFolderName <> "." And SubFolderName <> ".." Then 'Unfortunately, calling Dir with the vbDirectory attribute 'does not continually return subdirectories (only the first time); 'so you have to use the GetAttr function (which is covered in Help) 'to test, each time, that this is a folder and not a file If (GetAttr(CurFolderName & SubFolderName) _ And vbDirectory) = vbDirectory Then 'Up the array size by one ReDim Preserve SubFolderList(UBound(SubFolderList) + 1) 'Add the new folder to the array SubFolderList(UBound(SubFolderList)) = SubFolderName StatusBar = "Reading Subfolders... (" _ & CurFolderName & ": -> " & SubFolderName & ")" End If End If 'Get the next directory SubFolderName = Dir$() Loop 'Sort the list with the subfolders. If UBound(SubFolderList) > 0 Then WordBasic.SortArray SubFolderList() End If 'Now get all the subfolders of the current folder, then all the subfolders 'of each of those subfolders, and so on, up the directory tree, until there are no more subfolders. By recursively (repeatedly applying the procedure to successive results) calling the current function. 'If the current folder contains no subfolders, the following For .. Next loop gets skipped For Counter = 1 To UBound(SubFolderList) 'Up the size of the AllSubFoldersArray array by one ReDim Preserve AllSubFoldersArray(UBound(AllSubFoldersArray) + 1) 'Set the next item in the AllSubFoldersArray to be 'the next subfolder of the current folder AllSubFoldersArray(UBound(AllSubFoldersArray)) = _ CurFolderName & SubFolderList(Counter) & "\" 'Now run the this function recursively on that subfolder, 'to get its subfolders, if it has any AllSubFoldersArray = funcGetAllSubfolders(AllSubFoldersArray) Next Counter 'Set the complete directory structure as the function's Ni pht hnh: www.giaiphapexcel.com return value.

171

L p trnh VBA for Word


funcGetAllSubfolders = AllSubFoldersArray On Error GoTo 0 End Function

Notes
1. The funcGetAllSubfolders function is recursive; (recursive means repeatedly applying the same procedure to successive results). So it reads the names of all the folders in the folder you specify; then for each of these folders in turn, it calls itself in order to read their subfolders, and while it's in the process of doing that, it repeatedly calls itself in order to read the subfolders of those subfolders, and so on. Therefore, many instances of the function run simultaneously, and in each instance of the function, all the variables have different values from every other instance. Somehow, the code manages to keep track of all this, but it's more or less impossible to keep track of them when stepping through the code; which makes recursive procedures a nightmare to debug Luckily, you don't need to, because the function works! Partly for his reason, and partly because recursive procedures can easily run out of memory, if not very well coded they are usually best avoided. But in this case, to do it any other way would be much more complicated, so it's justified. And the macro runs very fast! 2. Usually it's best to define array variables as a data type other than a Variant (such as a String), as this can save a lot of memory. However, when passing arrays to functions, it's much simpler to define them as a Variant.

13. How to ensure (using VBA) that all your Word add-ins are installed in the correct path
Article contributed by Dave Rado Unfortunately, Word 2000 and 2002 use two Startup paths; the one that add-ins are supposed to be installed in, which is shown under Tools + Options + File Locations; and another one located in the program's installation folder. Even more unfortunately, many commercial add-ins install themselves in the wrong path or even worse, in both paths, which can result in you or your users having duplicate menus and toolbars, and the unfortunate sensation of seeing double! This causes all sorts of unwanted headaches; for more details see What do Templates and Add-ins store? To prevent this problem from arising, you could put the following macro into either Normal.dot (if it's for your own use only), or into an add-in that is installed in the correct Startup path; and you could call it from your AutoExec procedure.
Sub EnsureAddinsInCorrectPath() Dim WrongStartupPath As String, DecPlace As Long, AppVer As String, _ oAddin As AddIn, Counter As Long, AddinFileArray() As String ReDim AddinFileArray(100)

Ni pht hnh: www.giaiphapexcel.com

172

L p trnh VBA for Word


'-------------------'Get the application version DecPlace = InStr(Application.Version, ".") AppVer = Left$(Application.Version, DecPlace + 1) '-------------------'Get the "Office installation folder startup path" by adding "Startup\" to the Program path WrongStartupPath = System.PrivateProfileString("", _ "HKEY_CURRENT_USER\Software\Microsoft\Office\" & AppVer & "\Word\Options", "ProgramDir") '-------------------If Not Right$(WrongStartupPath, 1) = "\" Then WrongStartupPath = WrongStartupPath & "\" End If WrongStartupPath = WrongStartupPath & "Startup" & "\" '-------------------AddinFileArray(0) = Dir$(WrongStartupPath & "*.dot") '-------------------'Quit if no files found If Len(AddinFileArray(0)) = 0 Then Exit Sub '-------------------'If files found, get all their names and put them into an array Do While Not Len(AddinFileArray(Counter)) = 0 AddinFileArray(Counter) = AddinFileArray(Counter) Counter = Counter + 1 AddinFileArray(Counter) = Dir$ Loop '-------------------ReDim Preserve AddinFileArray(Counter - 1) 'Now delete or move all add-ins found in the wrong Startup path For Counter = 0 To UBound(AddinFileArray) 'First unload the add-in _ (Resume Next in case the user has removed it using Tools + Templates and Add-ns On Error Resume Next Set oAddin = AddIns(WrongStartupPath & AddinFileArray(Counter)) If oAddin.Installed Then oAddin.Installed = False oAddin.Delete End If On Error GoTo 0 '-------------------'Check whether the same file is also in the correct startup path If Len(Dir$(Options.DefaultFilePath(wdStartupPath) & "\" & _ AddinFileArray(Counter))) > 0 Then 'It's in both paths, so delete the file that's in the wrong path KillProperly Killfile:=WrongStartupPath & AddinFileArray(Counter) Else 'It's only in the wrong path, so move it to the right one and then reload it FileCopy WrongStartupPath & AddinFileArray(Counter), _ Options.DefaultFilePath(wdStartupPath) & "\" & AddinFileArray(Counter) KillProperly Killfile:=WrongStartupPath & AddinFileArray(Counter) Ni pht hnh: www.giaiphapexcel.com

173

L p trnh VBA for Word


AddIns.Add Options.DefaultFilePath(wdStartupPath) & "\" & AddinFileArray(Counter) End If Next Counter End Sub

Public Sub KillProperly(Killfile As String) If Len(Dir$(Killfile)) > 0 Then SetAttr KillFile, vbNormal Kill KillFile End If End Sub

The reason that the KillProperly subroutine is required, as opposed to just using the Kill statement, is covered in the article: How to delete files using VBA, including files which may be readonly. If you want to be really safe (and if you plan to store the above code in an add-in, rather than in Normal.dot), you could call it like this:
Public Sub AutoExec() If LCase$(ThisDocument.Path) = LCase$(Options.DefaultFilePath(wdStartupPath)) Then Call EnsureAddinsInCorrectPath End If End Sub

That ensures that if a copy of the add-in that contains this code somehow finds its way into the wrong startup path, the add-in won't try to delete itself.

Ni pht hnh: www.giaiphapexcel.com

174

L p trnh VBA for Word

CHAPTER 11: Miscellaneous

1. How can I get the mousewheel working in the VBA editing window?
Article contributed by Jonathan West The short answer is "Because sometimes Microsoft's right hand doesn't know what it's left hand is doing." The longer version is that when Microsoft first produced mice with wheels, they also invented a new kind of message in Windows, called WM_MOUSEWHEEL, which software developers could use to detect movements of the mouse wheel. Microsoft's Intellipoint mouse driver software generates these messages. But of course, in those early days, no software knew what to do with WM_MOUSEWHEEL messages, so Microsoft had the early versions of Intellipoint also generate WM_SCROLL messages, which were already defined, and which would result in windows being scrolled. But current versions of Intellipoint don't do this any more. I believe that the idea was that software companies would be told about this, and so would have time to sort out their software and issue updates that knew how to cope with WM_MOUSEWHEEL messages by the time Microsoft made the change. Unfortunately, they either forgot to tell the people responsible for the VBA editor, or those responsible never got round to making the necessary changes. However there is a workaround, in the form of utilities that detect WM_MOUSEWHEEL messages and generate the matching WM_SCROLL messages. I personally use VBScroll, a handy little free utility that gets the mousewheel working in the VBA editor. It works in the VBA editor for all the Office application (not just Word) and also in VB5 and VB6. You can download it from the following location. http://www.gasanov.net/VBScroll.htm Another Word MVP uses FreeWheel, which does the same job, and can be downloaded from here. http://www.geocities.com/SiliconValley/2060/freewheel.html

Ni pht hnh: www.giaiphapexcel.com

175

L p trnh VBA for Word

2. How to do a mail merge to the printer using VBA, without displaying the Print dialog
Or in the case of Word 2002, how to do the opposite! Article contributed by Ibby and Dave Rado In Word 97 and 2000, if you do a mail merge to the printer using:
ActiveDocument.MailMerge.Destination = wdSendToPrinter

... the Print dialog is displayed. The only way to prevent that from happening is as follows:
ActiveDocument.MailMerge.Destination = wdSendToNewDocument 'The ActiveDocument is now the merged document, 'not the main document ActiveDocument.PrintOut Background:=False ActiveDocument.Close wdDoNotSaveChanges

In Word 2002, because of all the complaints, Microsoft changed the behaviour so that ActiveDocument.MailMerge.Destination = wdSendToPrinter no longer displays the Print dialog. But of course, some people want the dialog to be displayed, so that the user can choose the printer, the page range, and so on; so in that scenario, the workaround is similar to the above code:
ActiveDocument.MailMerge.Destination = wdSendToNewDocument 'The ActiveDocument is now the merged document, 'not the main document Dialogs(wdDialogFilePrint).Show ActiveDocument.Close wdDoNotSaveChanges

This change in behaviour also means that if you have some users with Word 2002, and other users with earlier versions, you will either have to test for the version of Word in use, and run different code depending on the version, or bite the bullet and merge to a new document and print from there in all cases. (You can use Application.Version to test which version if Word is in use). Maybe in the next version of Word, we might actually be given the choice, with something like wdSendDirectToPrinter and wdSendToPrintDialog!

Ni pht hnh: www.giaiphapexcel.com

176

L p trnh VBA for Word

3. How to do a screen capture using VBA


Or: How to invoke the print screen command Article contributed by Lutz Gentkow

3.1. Using SendKeys


Unfortunately, you can't invoke the print screen command directly SendKeys "{PrtSc}" is not supported in VBA. But as it so frequently does, like a knight in shining armour, WordBasic rides the rescue:
Sub PrintScreen() WordBasic.SendKeys "{1068}" End Sub

(For some strange reason, SendKeys "{1068}" does not work). The above code works internationally, but if you don't mind your code being language-specific, then for English versions of Word you can use:.
Sub PrintScreen() WordBasic.SendKeys "{prtsc}" End Sub

for German versions of Word you can use:


Sub PrintScreen() WordBasic.SendKeys "{druck}" End Sub

And so on. (Oddly enough, SendKeys "{prtsc}", etc. did not work in Word 6/95; you had to use the keyboard code in those days; so why it should work with the WordBasic object in Word VBA is a mystery). In the case of the German version, but for some strange reason, not the English or international (1068) versions, you can also use this method to screen capture the active Window (equivalent to pressing Alt + PrtSc) by preceding the SendKeys string with %, as follows:
Sub PrintScreen() WordBasic.SendKeys "%{druck}" End Sub

Note that this is case sensitive; druck must be lower case (otherwise it will be treated as Shift + druck)

3.2. Using API calls


Ni pht hnh: www.giaiphapexcel.com

177

L p trnh VBA for Word Most serious programmers try to avoid using SendKeys if they can, because it has a reputation for unreliability. If you would prefer to avoid using SendKeys, or if you are not using a German version of Word but want to be able to capture the active Window, you can use API calls to capture either the screen or the current window, as follows. The following code sample is a much simplified version of the code posted in Microsoft Knowledge Base article Q240653: To capture the screen
Option Explicit Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal _ bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long) Private Const VK_SNAPSHOT = &H2C Sub PrintScreen() keybd_event VK_SNAPSHOT, 1, 0, 0 End Sub

To capture the active window


Option Explicit Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal _ bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long) Private Const KEYEVENTF_KEYUP = &H2 Private Const VK_SNAPSHOT = &H2C Private Const VK_MENU = &H12 Sub AltPrintScreen() keybd_event VK_MENU, 0, 0, 0 keybd_event VK_SNAPSHOT, 0, 0, 0 keybd_event VK_SNAPSHOT, 0, KEYEVENTF_KEYUP, 0 keybd_event VK_MENU, 0, KEYEVENTF_KEYUP, 0 End Sub

4. How to get the username of the current user


Article contributed by Astrid Zeelenberg If you want a routine that works for all types of networks, you'll have to use an API call to show the username of an user. The following code, which needs to be in a Module, was taken from Microsoft Knowledge Base article Q161394.
Option Explicit 'Declare for call to mpr.dll. Declare Function WNetGetUser Lib "mpr.dll" _ Ni pht hnh: www.giaiphapexcel.com

178

L p trnh VBA for Word


Alias "WNetGetUserA" (ByVal lpName As String, _ ByVal lpUserName As String, lpnLength As Long) As Long Const NoError = 0 'The Function call was successful

Function GetUserName() As String 'Buffer size for the return string. Const lpnLength As Long = 255 'Get return buffer space. Dim status As Integer 'For getting user information. Dim lpName, lpUserName As String 'Assign the buffer size constant to lpUserName. lpUserName = Space$(lpnLength + 1) 'Get the log-on name of the person using product. status = WNetGetUser(lpName, lpUserName, lpnLength) 'See whether error occurred. If status = NoError Then 'This line removes the null character. Strings in C are null'terminated. Strings in Visual Basic are not null-terminated. 'The null character must be removed from the C strings to be used 'cleanly in Visual Basic. lpUserName = Left$(lpUserName, InStr(lpUserName, Chr(0)) - 1) End If 'Display the name of the person logged on to the machine. GetUserName = lpUserName End Function

You could call it like this:


Sub Test() MsgBox GetUserName End Sub

Note that if the user is not logged in, the function will return nothing (an empty string).

Ni pht hnh: www.giaiphapexcel.com

179

L p trnh VBA for Word

5. Useful WordBasic commands that have no VBA equivalent


Article contributed by Jonathan West When Microsoft released Word 97, a new programming language VBA replaced the WordBasic language that had been available in earlier versions of Word. For most things, VBA is a much more powerful and flexible programming language than WordBasic, but there are a few very useful WordBasic commands which have no direct equivalents in VBA. Fortunately, VBA includes the WordBasic object, which gives access to most of the old WordBasic commands.

5.1. SortArray
This is perhaps the most useful of the commands left behind. It allows you to sort the elements of an array using a single line of code. At its simplest, you can use it on a one-dimensional array as follows.
Sub SortTest() Dim ss(2) As String Dim i As Long ss(0) = "orange" ss(1) = "apple" ss(2) = "banana" WordBasic.SortArray ss() For i = 0 To 2 Debug.Print ss(i) Next i End Sub

This sorts the array in ascending alphabetical order However, you can also sort in descending order, and sort either dimension of a two-dimension array. The full list of the SortArray arguments is as follows
SortArray ArrayName[$]() [, Order] [, From] [, To] [, SortType] [, SortKey]

ArrayName is the name of the array Order is 0 for ascending (by default), 1 for descending From is the first element to sort (0 by default) To is the last element to sort (by default the last element of the array) SortType determines whether you are sorting rows or columns. 0 (default) for rows, 1 for columns SortKey is applicable only to two-dimensional arrays, and indicates the row or column used as
Ni pht hnh: www.giaiphapexcel.com

180

L p trnh VBA for Word the sort key. It is 0 by default Note that, unlike most VBA methods, you don't use named arguments with this command; thus you can have
WordBasic.SortArray MailingList$(), 1, 1, 20, 0, 1

but not
WordBasic.SortArray ArrayName:=MailingList$(), Order:=1, From:=1, To:=20, _ SortType:=0, SortKey:=1

Also, you cannot miss out arguments if you want to use later ones, thus you can have
WordBasic.SortArray Test(), 0, 0, 2, 0, 1

but not
WordBasic.SortArray Test(), 0, , , , 1

There is one other limitation of the SortArray command. It will sort an array declared as such, but it will not sort an array that is contained in a Variant. If you create an array like this:
Dim vArray as Variant vArray = Array("orange", "apple", "banana")

SortArray will not sort it. (Also if you do not declare your array at all, it will be treated as a variant and will not be sorted).

5.2. FileNameInfo$()
This is another very useful function for which there is no direct VBA equivalent. FileNameInfo allows you to get just the filename or a fully qualified pathname from a filename given to it. The nearest equivalent in VBA are the Name, FullName and Path properties of the Document object. FileNameInfo is different in that you don't need to have the document open. The syntax is
x = WordBasic.FilenameInfo$(Filename$, FileType)

where Filename is the name of the file, and FileType is a number which defines the part of the filename you want to return:

Ni pht hnh: www.giaiphapexcel.com

181

L p trnh VBA for Word 1 - the full pathname, e.g. C:\My Documents\My File.doc" 2 - the filename only, if the file is in the current folder, otherwise the full pathname 3 - the filename only 4 - the filename without the extension 5 - the path without the filename 6 - the UNC pathname One case where FileNameInfo$ is very useful is to get the pathname of a file which has just been selected by the user in the FileOpen dialog. The following code returns the full pathname of a file selected by the user.
With Dialogs(wdDialogFileOpen) If .Display Then MsgBox WordBasic.FilenameInfo$(.Name, 1) Else MsgBox "No file selected" End If End With

5.3. DisableAutoMacros
If you are running a macro that opens (or creates) several files, the last thing you may want is for an AutoOpen (or AutoNew) macro to fire up each time. WordBasic has a means of preventing this, which VBA never copied.
WordBasic.DisableAutoMacros 1 WordBasic.DisableAutoMacros 0 'Disables auto macros 'Enables auto macros

This command is also very useful when launching an instance of Word from another application, or from VB, when you will generally not want any AutoExec macros to fire.

5.4. ToolsBulletsNumbers
WordBasic allows you to remove all manually typed numbering from a selection using the old Word 2 command:
WordBasic.ToolsBulletsNumbers Replace:=0, Type:=1, Remove:=1

This is particularly useful for removing manually typed numbering from Headings in a document you have been emailed, prior to applying List Numbering. If you go into Outline View, set the Heading Level to the number of levels you need to remove the typed numbering from, and run the above line, it will just remove numbering from those Headings and will leave the body text alone. Or you can use the following macro to do the same thing:
Sub RemoveNumbersFromHeadings() Dim ViewType As Long, ShowHeadingLevel As Long, MyRange As Range Ni pht hnh: www.giaiphapexcel.com

182

L p trnh VBA for Word


Application.ScreenUpdating = False 'Set Range variable to current selection so it can be returned to at the end Set MyRange = Selection.Range 'Set variable to current View so it can be returned to at the end ViewType = ActiveWindow.View.Type 'Switch to Outline View ActiveWindow.View.Type = wdOutlineView 'Checks the state (using the ID in case the toolbar has been customised) of _ all the ShowHeadings buttons on the Outline toolbar; if none are depressed, _ then it must be set to ShowAll. Stores result in a variable so that _ the same level can be returned to at the end of the macro For ShowHeadingLevel = 71 To 77 If CommandBars.FindControl(ID:=ShowHeadingLevel).State Then ShowHeadingLevel = (ShowHeadingLevel - 70) Exit For End If Next ShowHeadingLevel 'if none of the heading level buttons depressed sets variable to 1 If ShowHeadingLevel = 78 Then ShowHeadingLevel = 1 ActiveWindow.View.ShowHeading 3 ActiveDocument.Select WordBasic.ToolsBulletsNumbers Replace:=0, Type:=1, Remove:=1 If ShowHeadingLevel = 0 Then ActiveWindow.View.ShowAllHeadings Else ActiveWindow.View.ShowHeading ShowHeadingLevel End If ActiveWindow.View.Type = ViewType MyRange.Select Set MyRange = Nothing Application.ScreenUpdating = False End Sub

5.5. FileCopy/FileCopyA
The VBA FileCopy statement will not copy files that are open. However, the WordBasic equivalent will (this is what is known as progress!). Unfortunately, the syntax of WordBasic equivalent is different in Word 97 and Word 2000!
Ni pht hnh: www.giaiphapexcel.com

183

L p trnh VBA for Word The following works even if the file being copied is open:
If Left$(Application.Version, 1) = "8" Then 'Word 97 WordBasic.CopyFile FileName:="c:\OldDirectory\Temp.doc", _ Directory:="C:\NewDirectory\Temp.doc" Else 'Word 2000 and above WordBasic.CopyFileA FileName:="c:\OldDirectory\Temp.doc", _ Directory:="C:\NewDirectory\Temp.doc" End If

5.6. FileProperties
If you want to intercept the FileProperties menu command, the only reliable way to do it is to use:
Sub FileProperties() 'your code here WordBasic.FileProperties End Sub

In fact, if you let Word create the code for you, using the method described here, the above code will be created.

5.7. SendKeys
If you want to do a screen capture using VBA, (simulating the PrtScr key), you have to use:
WordBasic.SendKeys "%{1068}"

6. How to send an email from Word using VBA


Article contributed by Astrid Zeelenberg

6.1. Using the Routing Slip method


The easiest way to send a document by email is to use the Word's built-in RoutingSlip method. With this method you can send the document to one or more recipients and you can set the subject for the message, and choose whether it should be send to all the recipients at once, or be routed from one to the next. The advantages of using the RoutingSlip are that:

The code will work with all email programs, so you don't need to know whether the system that's running this code has any specific email program installed 184

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word


The document can be sent even if it hasn't been saved Your code will run faster than if you automate Outlook (unless Outlook already happens to be open when your code runs).

Disadvantages:

The email message has default body text, which you can't change from within code. You can't use the text inside the document as the body of the email; you can only send the document as an attachment. You can't set any recipients for the bcc field.

The code for using the RoutingSlip method is:


Activedocument.HasRoutingSlip = True With Activedocument.RoutingSlip .Subject = "New subject goes here" .AddRecipient "Firstaddress@Mail.com" .AddRecipient "Secondaddress@Mail.com" .Delivery = wdAllAtOnce End With Activedocument.Route

6.2. Automating Outlook


The other option is to automate Outlook to send your document. The main disadvantage, of course is that you need to be absolutely sure that the system that's running your code has Outlook installed. Another disadvantage is that if you want to send the document as an attachment the document needs to have been saved at least once before you can send it. This is because you need a path and filename for the file in the code. If Outlook is not already open when your code runs, this method will also be slower. Finally, the code is also a bit more complex then using .Route method. You'll need to set a reference (Tools-References in the Visual Basic Editor) to the Outlook type Library to get this code to work:
Sub SendDocumentInMail() Dim bStarted As Boolean Dim oOutlookApp As Outlook.Application Dim oItem As Outlook.MailItem On Error Resume Next 'Get Outlook if it's running Set oOutlookApp = GetObject(, "Outlook.Application") If Err <> 0 Then 'Outlook wasn't running, start it from code Set oOutlookApp = CreateObject("Outlook.Application") bStarted = True Ni pht hnh: www.giaiphapexcel.com

185

L p trnh VBA for Word


End If 'Create a new mailitem Set oItem = oOutlookApp.CreateItem(olMailItem) With oItem 'Set the recipient for the new email .To = "recipient@mail.com" 'Set the recipient for a copy .CC = "recipient2@mail.com" 'Set the subject .Subject = "New subject" 'The content of the document is used as the body for the email .Body = ActiveDocument.Content .Send End With If bStarted Then 'If we started Outlook from code, then close it oOutlookApp.Quit End If 'Clean up Set oItem = Nothing Set oOutlookApp = Nothing End Sub

That sends the text in the document as the content of the email (not as an attachment); and it sends it as plain text, so all formatting is lost. You can send the document as attachment using Outlook provided the document has been saved at least once:
Sub SendDocumentAsAttachment() Dim bStarted As Boolean Dim oOutlookApp As Outlook.Application Dim oItem As Outlook.MailItem On Error Resume Next If Len(ActiveDocument.Path) = 0 Then MsgBox "Document needs to be saved first" Exit Sub End If Set oOutlookApp = GetObject(, "Outlook.Application") If Err <> 0 Then Set oOutlookApp = CreateObject("Outlook.Application") bStarted = True End If Ni pht hnh: www.giaiphapexcel.com

186

L p trnh VBA for Word


Set oItem = oOutlookApp.CreateItem(olMailItem) With oItem .To = "recipient@mail.com" .Subject = "New subject" 'Add the document as an attachment, you can use the .displayname property 'to set the description that's used in the message .Attachments.Add Source:=ActiveDocument.FullName, Type:=olByValue, _ DisplayName:="Document as attachment" .Send End With If bStarted Then oOutlookApp.Quit End If Set oItem = Nothing Set oOutlookApp = Nothing End Sub

7. How to get the most recently used document to be opened automatically when you open Word
Article contributed by Astrid Zeelenberg, Dave Rado and Will Rickards

1. Using a command line switch to open the most recently used document
You can open Word and have the most recently used (MRU) file opened automatically by clicking on the Windows Start button, selecting Run, and typing: winword.exe /mFile1. The /m switch normally means run a macro called [whatever follows the m], but it can also be used to run almost any WordBasic command, and File1 is the WordBasic command to open the MRU file. The only WordBasic commands which don't appear to work when following the /m switch are those which rely on a document already being open. Alternatively, you can create a new shortcut to Word; right-click on it and select Properties; and on the Shortcut tab, where it says Target, add /mFile1 to the end of the path, so it looks like this:
"C:\Program Files\Microsoft Office\Office\WINWORD.EXE" /mFile1

Make sure that the path of Winword.exe is still set to the folder where Office is installed. You can now use the new shortcut when you want to start Word with the most recently used file
Ni pht hnh: www.giaiphapexcel.com

187

L p trnh VBA for Word and your usual shortcut when you don't. However, note that using the /m switch prevents any AutoExec macros you may have from running. Also, if the MRU file is inaccessible or has been deleted or renamed, this method won't find the most recently used file that does exist; it will just create a blank document. If you want it to open the most recently used valid file, or if you don't want to disable your AutoExec macros, you'll need to use a macro.

2. Using a macro to open the most recently used document whenever Word opens
Paste the following code into a Global template and it will run automatically whenever Word opens:
Sub AutoExec() Dim oRecentFile As RecentFile If RecentFiles.Count >= 1 Then 'First delete any invalid files from the recent files list. On Error Resume Next 'in case one of the drives is an invalid drive For Each oRecentFile In RecentFiles If Len(Dir(oRecentFile.Path & "\" & oRecentFile.Name)) = 0 Then 'If the file doesn't exist, delete it from the list RecentFiles(oRecentFile.Index).Delete End If Next oRecentFile Set oRecentFile = Nothing On Error GoTo 0 If RecentFiles.Count >= 1 Then RecentFiles(1).Open End If End If End Sub

The AutoExec macro opens the first valid file that's listed in the MRU (Most Recently Used) list: For more on AutoExec macros see: Writing application event procedures. Note that if you also want the selection to return to your last editing point, you can add the line:
Application.GoBack

... just after the line:


RecentFiles(1).Open

Ni pht hnh: www.giaiphapexcel.com

188

L p trnh VBA for Word

3. Using a macro to open the most recently used document whenever Word is opened from its icon, but not when you open Word by launching a file
Paste the following code into a module in a Global template:
Option Explicit Public Declare Function GetCommandLine Lib "kernel32" _ Alias "GetCommandLineA" () As Long Public Declare Function lstrcpy Lib "kernel32" _ Alias "lstrcpyA" (ByVal lpString1 As String, _ ByVal lpString2 As Long) As Long Public Declare Function lstrlen Lib "kernel32" _ Alias "lstrlenA" (ByVal lpString As Long) As Long

Sub AutoExec() Dim oRecentFile As RecentFile Dim strCommandLine As String If RecentFiles.Count >= 1 Then 'First delete any invalid files from the recent files list. On Error Resume Next 'in case one of the drives is an invalid drive For Each oRecentFile In RecentFiles If Len(Dir(oRecentFile.Path & "\" & oRecentFile.Name)) = 0 Then 'If the file doesn't exist, delete it from the list RecentFiles(oRecentFile.Index).Delete End If Next oRecentFile Set oRecentFile = Nothing On Error GoTo 0 'get the commandline by calling the CmdLinetoString function, which follows strCommandLine = CmdLinetoString(GetCommandLine()) If Len(strCommandLine) <= Len(Chr(34) & Application.Path & _ "\" & "winword.exe" & Chr(34) & " ") Then 'Following only runs if Word was launched from its icon If RecentFiles.Count >= 1 Then RecentFiles(1).Open End If End If End If End Sub Ni pht hnh: www.giaiphapexcel.com

189

L p trnh VBA for Word

Public Function CmdLinetoString(ByVal lngPtr As Long) As String Dim strReturn As String Dim StringLength As Long 'get the length of the string (not including the terminating null character) StringLength = lstrlen(lngPtr) 'initialize our string so it has enough characters including the null character strReturn = String$(StringLength + 1, 0) 'copy the string we have a pointer to into our new string lstrcpy strReturn, lngPtr 'now strip off the null character at the end strReturn = Left$(strReturn, StringLength) 'return the string CmdLinetoString = strReturn End Function

Again, if you also want the selection to return to your last editing point, just add the line:
Application.GoBack

... immediately after the line:


RecentFiles(1).Open

4. The Application.GoBack bug and how to get round it


See the article: GoBack (Shift+F5) doesn't work in some newly-opened documents

8. Creating upside down or rotated text in Word


Article contributed by Dave Rado and Suzanne S. Barnhill Unfortunately, unlike PowerPoint, Word does not allow you to rotate or flip text. Even if you create a text box, rotate or flip it in PowerPoint and paste it into Word as an MS Office Drawing Object, it comes into Word the right way up thus proving that there's no such thing as an MS Office Drawing Object; there are PowerPoint drawing objects, and there are Word drawing objects. However, there are a number of workarounds for this.

8.1. Using two table cells


If you want to print something like a card that needs to be folded in half, with half of it therefore needing to be upside down, set your page up as landscape, create a single row, 2-cell borderless table, drag the row-height so that it fills the page, and set the Text Direction (right-click menu) of 190 Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word each cell to vertical orientations; one cell going bottom-to-top, and the other cell going top-tobottom. And you're done.

8.2. Word 2002 only: Rotating a picture of your text


In Word 2002, you can use the following procedure: 1. Type the text you want to rotate; select it; Cut, and Paste Special As Picture. 2. Make the picture Floating, if it isn't already (e.g. choose Text Wrapping on the Picture toolbar and change the wrapping to something other than In Line With Text.) 3. If you click on the picture, you can now free-rotate it, or flip it, or rotate left or right, or set the rotation to a specific angle using the Format + Picture dialog. Be aware, though, that if you send your Word document to someone who is using an earlier version of Word than version 2002, the picture (and the text in it) will no longer be rotated. So if you want back-compatibility, you will need to use one of the other methods described below. You will not lose any print quality by pasting as a picture in this way, because the text in the picture is stored internally as text (albeit uneditable), not as curves. It is also possible to rotate ot flip the picture in Word 2002 while the picture is Inline. This (undocumented) feature creates a strange hybrid graphic, part Floating, part Inline. For more details, see: Inline-Floating hybrid graphics in Word 2002.

8.3. Pasting from PowerPoint into Word as a picture


For most requirements, probably the simplest workaround is to create your rotated or flipped text in PowerPoint text boxes, and paste them into Word as pictures. Having created the text box(es), select Format + Text Box, and on the Text Box tab, set all the internal margins to 0, and also turn off Word wrap text in autoshape. This will prevent any unnecessary white space from appearing when you paste into Word. Then type your text, rotate or flip them as desired using the Draw menu on the Drawing toolbar. Now, copy the text box(es), and in Word, select Paste Special As Picture or slightly better, As Picture (Enhanced Metafile). Make sure you paste it in inline. And you're done. Note that you will not lose any print quality by pasting as a picture in this way, because the text in the picture is stored internally as text (albeit uneditable), and not as curves. But don't use a bitmap editor (such as Photoshop or Paintshop Pro) to create your upside down text, because that will reduce the print quality of your text, unless you save the graphic at a very high resolution indeed (and doing that would mean a large file size, so it's far better to use a vector package such as PowerPoint). Of course, because it has been pasted into Word as a picture, it is no longer editable; so it's a good idea to keep a PowerPoint boilerplate file in which to store the originals. Then, if you ever need to edit them, you can do so into PowerPoint, and paste back into Word as a picture.
Ni pht hnh: www.giaiphapexcel.com

191

L p trnh VBA for Word If you want to do it programmatically You may need to insert upside down text programmatically. For example, you may have a Label template with an AutoNew macro in which you capture the text to be inserted from a UserForm or a database; and part of the label gets folded back; so that some of the text, such as the title or company name, needs to be upside down on the back of the fold. For this, you could use something like the code sample at How to control PowerPoint from Word. Unfortunately, unless PowerPoint is already open when your macro starts, this is quite a slow way of doing it (because of the time taken to open PowerPoint): On the other hand, if you have more than one location in the document in which you need to insert rotated text, this overhead will only apply once, provided you don't quit PowerPoint until all of your pictures have been pasted in.

8.4. Pasting from other vector graphics applications


You don't need to use PowerPoint, of course, you could use other vector applications, such as Visio, CorelDraw. and so on. I find PowerPoint easier for this sort of thing, though. Also, Visio does not support commonly used typographical characters such as dashes and smartquotes. Visio does have one advantage over using PowerPoint, however; if you paste a Visio text box into Word as a Visio object, it retains its rotation; so you could keep it in Word as an editable object (although only editable by people who also have the same or a higher version of Visio). However, OLE objects even Visio ones, although Visio objects are better-behaved than most will slow down your documents significantly, and increase the risk of document corruption; so should be used only if really needed. CorelDraw images should never be pasted into Word as objects, only as pictures, because CorelDraw objects will quickly bring your documents grinding to a halt. If you want to do it programmatically Most vector applications cannot be automated from within Word. Visio can, but not nearly as easily as PowerPoint, as it has no macro recorder

8.5. Using Word Art


The versions of WordArt that came with Word 2 through Word 95 were very useful indeed, allowing you to create rotated or flipped text that looked exactly like ordinary text other than being rotated or flipped. Unfortunately, in Word 97 and higher, this was replaced with a very different application, which can only produce fancy text. It's great for party invites, but not for serious use. Also, WordArt in Word 97 and higher can only be inserted as a Floating object; in order to make it Inline, you have to Cut, and Paste Special As Picture. If (like me) you still have one of the pre-Word 97 versions of WordArt installed, you can still use it; choose Insert + Object, and select Microsoft WordArt. I use Word 6 WordArt even more than I use PowerPoint, for creating things like rotated watermarks, and so on.

Ni pht hnh: www.giaiphapexcel.com

192

L p trnh VBA for Word If you want to do it programmatically Although I have written programs in the past that activate a Word 6 WordArt object and fill it with text using SendKeys, one couldn't use such code unless all one's users had Word 6 WordArt installed, an unlikely scenario, these days. If, despite the utter garishness of the text produced by Word 97 WordArt and higher, you want to create upside down or rotated WordArt text programmatically, you can use something like the following:
Sub InsertWordArtWatermark() Dim MyRange As Range, oShape As Shape Set MyRange = ActiveDocument.Sections(1).Headers(wdHeaderFooterPrimary). _ Range.Paragraphs(1).Range Set oShape = ActiveDocument.Sections(1). _ Headers(wdHeaderFooterPrimary).Shapes.AddTextEffect _ (PresetTextEffect:=msoTextEffect11, _ Text:="Confidential", _ FontName:="Arial", _ FontSize:=60, _ FontBold:=False, _ FontItalic:=False, _ Left:=0#, _ Top:=0#, _ Anchor:=MyRange) With oShape .RelativeHorizontalPosition = wdRelativeHorizontalPositionPage .RelativeVerticalPosition = wdRelativeVerticalPositionPage .Left = InchesToPoints(2) .Top = InchesToPoints(4.5) .Fill.ForeColor.RGB = RGB(128, 128, 128) .Line.ForeColor.RGB = RGB(128, 128, 128) .Line.BackColor.RGB = RGB(27, 27, 27) .Shadow.ForeColor.RGB = RGB(51, 51, 51) .IncrementRotation -45 .WrapFormat.Type = wdWrapNone .ZOrder msoSendBehindText End With End Sub

8.6. And just for the sake of completeness ...


Some people also create upside-down text using PostScript print fields. That method, however, is as intuitive as an Altair computer, creates documents that are near-impossible to maintain, and requires anyone you ever send your documents to to have a PostScript printer so I wouldn't recommend it.

Ni pht hnh: www.giaiphapexcel.com

193

L p trnh VBA for Word

9. How can I tile documents vertically in Word 2000?


Article contributed by Jay Freedman In Word 2000, each document lives in a separate window. This means that you can place them side by side with the Tile Vertically command from the Task Bar's right-click menu. But-there's always a gotcha if all other program windows aren't minimized, they'll be squeezed in among the Word documents. Of course there's a better way! It involves using a macro; so if you're not familiar with using macros, see What do I do with macros sent to me by other newsgroup readers to help me out? This macro starts by listing in an array all the open and non-minimized Word documents. When the list is complete, the macro knows how many there are. Next it grabs one of the windows and maximizes it, to get the width and height measurements of the full screen. (Through an API call, Windows can easily tell you these sizes in pixels, but Word wants to size its windows in points.) Our goal is to set each document window to a fraction of the total width that depends on how many documents are in the list. If there are two documents, each gets half of the full screen width; if there are three documents, each gets a third, and so on. Next, the macro runs through the list of windows, setting each to the same size. Each window is positioned (by setting its .Left property to a multiple of the desired window width) just to the right of the one before it. Finally, the macro activates the window that had the focus when the macro started. Unlike the Task Bar's Tile Vertically command, this window hasn't been forced to the leftmost position; all windows appear in the same order as they are listed on Word's Window menu.
Public Sub WindowTileVertical() Dim oWind As Window ' working object Dim nActiveWindowIndex As Long ' save to reactivate at end Dim nNonMinWindows() As Long ' list of nonminimized windows Dim nNonMinWindowsCount As Long ' length of the list Dim nScreenWidth As Long ' width & height of full Dim nScreenHeight As Long ' screen in points Dim nDesiredWidth As Long ' width each window should be Dim nIndex As Long ' For loop index ' Save active window's index nActiveWindowIndex = ActiveWindow.Index ' Find out how many Word windows are not minimized ' (i.e., maximized or normal), and store them in the nNonMinWindows array nNonMinWindowsCount = 0 For Each oWind In Windows If oWind.WindowState <> wdWindowStateMinimize Then nNonMinWindowsCount = nNonMinWindowsCount + 1 ReDim Preserve nNonMinWindows(nNonMinWindowsCount - 1) Ni pht hnh: www.giaiphapexcel.com

194

L p trnh VBA for Word


nNonMinWindows(nNonMinWindowsCount - 1) = oWind.Index End If Next oWind If nNonMinWindowsCount < 1 Then Exit Sub ' Maximize the current window, to get the ' screen width and height in points With ActiveWindow .WindowState = wdWindowStateMaximize nScreenWidth = .Width nScreenHeight = .Height End With ' The desired width is the screen width ' divided by the number of windows being tiled nDesiredWidth = nScreenWidth / nNonMinWindowsCount ' Set each window to the desired size. Offset each one ' horizontally to sit to the right of the one before. For nIndex = 0 To nNonMinWindowsCount - 1 Set oWind = Windows(nNonMinWindows(nIndex)) With oWind .Activate .WindowState = wdWindowStateNormal .Width = nDesiredWidth .Height = nScreenHeight .Top = 0 .Left = nIndex * nDesiredWidth End With Next nIndex Set oWind = Nothing Windows(nActiveWindowIndex).Activate End Sub

Ni pht hnh: www.giaiphapexcel.com

195

L p trnh VBA for Word

10. Invalid Page Fault message when running a macro


Or: Keeping Word VBA projects clean Article contributed by Bill Coan, Jonathan West and Cindy Meister Word templates get bloated through repeated editing and saving of the VBA code. Bloated templates can often throw invalid page faults for no apparent reason when running VBA code. Why Word doesn't clean up after itself when saving a template which contains a VBA project is one of the great mysteries of life. Excel suffers from the same problem. The way to fix this is to export and remove all the modules from the template, and then save the template. (Take a backup first!) Then re-import all the modules again, save the template, recompile and save the template again. If you have been editing the VBA over a long period, and saving many times, you may be astonished at how much smaller the new template is. How often should you clean a project? No one can answer that question for you. Each project is different. Larger projects generally need to be cleaned more often than smaller projects. One rule of thumb is to clean a project whenever it has grown to 1.5 times its normal size but this is a rough guideline only. Very large projects should be cleaned well before they reach this size. The Word Code Cleaner cannot remove bloat caused by extensive customizations of Command Bars or Key Bindings. If a project contains substantial customization of this sort, or if you frequently modify the customization and save the changes, the project will eventually become corrupt, even if you do regularly clean it with the Word Code Cleaner. Nor can the Word Code Cleaner remove bloat caused by proliferation of unused List Templates in a document or template. Any modifications to the numbering scheme in a document or template can quickly cause List Templates to proliferate and get propagated to new documents. The only way to avoid the bloat caused by customizations of Command Bars and Key Bindings and proliferation of unused List Templates is to recreate a project from scratch, starting with a new blank template that has never had its Command Bars or Key Bindings customized and never had any unused List Templates introduced. Serious developers may want to consider generating styles, including numbering, via code, so that styles can be recreated quickly when recreating a project from scratch. The same goes for generating page setup, including headers and footers, and for generating document variables and custom document properties. By doing all these things via code, youll save hours of work every time you decide that its time to recreate a project from scratch. In my own experience, Ive found that it is helpful to maintain both a user and a developer copy of each project. The developer copy contains all of the code for the project, including the code for recreating the project from scratch. The user copy doesnt contain the code for recreating the project. This allows the user copy to be kept as small as possible. Use the following steps to modify a project:
Ni pht hnh: www.giaiphapexcel.com

196

L p trnh VBA for Word Open the existing developer copy of the template for editing. Export all code modules, class modules, and the special ThisDocument class module. Close the developer copy of the template. Create a new, blank template. Import all the exported code into the new, blank template. Copy all the autotext items from the old template into the new one. Run macros to set up styles, page layout, headers/footers, document variables, custom document properties, commandbars, keybindings. 8. Insert the content of the old template into the new one. 9. Save the template as the new developer copy. 10. Delete the macros that setup styles, page layout, headers/footers, document variables, custom document properties, commandbars, keybindings. 11. Delete all comments and blank lines from the code. 12. Save the template as the new user copy. 13. Compile the code in the users copy, digitally sign it, and protect it for viewing. This can be a long and involved process. The Word Code Cleaner (see below) automates the portion involving export and import of code modules. In addition to cleaning your project this way, the Word Code Cleaner can optionally strip comments, blank lines, and spaces from your code. This allows you to achieve the minimum possible file size for your project. 1. 2. 3. 4. 5. 6. 7.

10.1. Obtaining the Word Code Cleaner


The Word Code Cleaner was originally written by Rob Bovey for Excel, then modified to run in Word by Robert Affleck, and has since been updated by Bill Coan. It will work with Word 97, Word 2000 and Word 2002. The Word Code Cleaner does not work with any version of Word for Macintosh. 1. 2. 3. 4. Click the Zip icon to download the file. Unzip it. Double-click the .exe file to install the addin. Start Word and choose Tools|Word Code Cleaner.

If the Word Code Cleaner doesn't fix your problem, have a look at WD2002: Part 1: Troubleshooting Problems When Word Has Encountered a Problem and Needs to Closewhich provides very detailed information on how to track down the causes of Office IPFs (any version).

Ni pht hnh: www.giaiphapexcel.com

197

L p trnh VBA for Word

11. Creating sequentially numbered documents (such as invoices)


Article contributed by Doug Robbins Use an Autonew macro to add a sequential number to a document and save it with that number. In the template from which you create the document, insert a bookmark named Order in the location where you want the sequential number to appear and create an AutoNew macro in the template, as follows:
Sub AutoNew() Order = System.PrivateProfileString("C:\Settings.Txt", _ "MacroSettings", "Order") If Order = "" Then Order = 1 Else Order = Order + 1 End If System.PrivateProfileString("C:\Settings.txt", "MacroSettings", _ "Order") = Order ActiveDocument.Bookmarks("Order").Range.InsertBefore Format(Order, "00#") ActiveDocument.SaveAs FileName:="path" & Format(Order, "00#") End Sub

If you don't need to display the number in the document, but just want to save it with a sequential number, there is no need to create the bookmark in the template and you should then delete the second last line of the code. Note: You may see postings elsewhere which recommend using the registry instead of a text file to store the number. If you use the Registry in this way, the user is tied to one machine. For instance, if more than one user needs access to the up-to-date number, a text file can be saved in a shared area on the network; a Registry setting can't. Also, if users need to be able to log on to more than one machine and the text file is in their user area on the network, they can have access to it from any machine. Even if it's stored locally, if they upgrade to a new machine one day, the text file can just be copied to their new machine (much easier than copying Registry settings). Another point worth considering: if there is any possibility that the user might one day need to amend the number manually for any reason, amending a text file is easy; letting a user loose on the Registry is scary! 198

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word

12. Sequentially numbering multiple copies of single document using a macro


Article contributed by Doug Robbins Create a bookmark named SerialNumber in the document where you want the Serial Number to appear. It can be in the header or footer if that is where you want the number. Then create a macro containing the following commands to print the document. It will ask for the number of copies that you want to make and sequentially number each copy.The first time this macro runs, the first copy will be numbered 1 and when it finishes running, it will store in aSettings.Txt file the number that is one more that the number on the last copy.The next time the macro is run, it will start numbering the copies from that number. If when you first start, you want the numbers to start at some number other than 1, run the macro, entering 1 as the number of copies and then open Settings.Txt file and replace the number in the file with the number that you want as the first in the series.At any time thereafter, if you want the series to start at a particular number, you can open that file and replace the number in it with the number that you want to be the first in the series.
Dim Message As String, Title As String, Default As String, NumCopies As Long Dim Rng1 As Range ' Set prompt. Message = "Enter the number of copies that you want to print" ' Set title. Title = "Print" ' Set default. Default = "1" ' Display message, title, and default value. NumCopies = Val(InputBox(Message, Title, Default)) SerialNumber = System.PrivateProfileString("C:\Settings.Txt", _ "MacroSettings", "SerialNumber") If SerialNumber = "" Then SerialNumber = 1 End If Set Rng1 = ActiveDocument.Bookmarks("SerialNumber").Range Counter = 0 While Counter < NumCopies Rng1.Delete Rng1.Text = SerialNumber ActiveDocument.PrintOut SerialNumber = SerialNumber + 1 Counter = Counter + 1 Wend 'Save the next number back to the Settings.txt file ready for the next Ni pht hnh: www.giaiphapexcel.com

199

L p trnh VBA for Word


use. System.PrivateProfileString("C:\Settings.txt", "MacroSettings", _ "SerialNumber") = SerialNumber 'Recreate the bookmark ready for the next use. With ActiveDocument.Bookmarks .Add Name:="SerialNumber", Range:=Rng1 End With ActiveDocument.Save

If you want the Serial Number to appear in a particular format, e.g. 001, 002, etc, replace the line
Rng1.Text = SerialNumber

with
Rng1.Text = Format(SerialNumber, "00#")

13. How to remove manually typed numbering from a document


Article contributed by Dave Rado You can use the old Word 2 command:
WordBasic.ToolsBulletsNumbers Replace:=0, Type:=1, Remove:=1

If not familiar with using macros, see What do I do with macros sent to me by other newsgroup readers to help me out? This command is particularly useful for removing manually typed numbering from Headings in a document you have been emailed, prior to applying List Numbering. If you go into Outline View, set the Heading Level to the number of levels you need to remove the typed numbering from, and run the above line, it will just remove numbering from those Headings and will leave the body text alone. Or you can use the following macro to do the same thing:
Sub RemoveNumbersFromHeadings() Dim ViewType As Long, ShowHeadingLevel As Long, MyRange As Range Application.ScreenUpdating = False 'Set Range variable to current selection so it can be returned to at the end Set MyRange = Selection.Range 'Set variable to current View so it can be returned to at the end ViewType = ActiveWindow.View.Type 'Switch to Outline View ActiveWindow.View.Type = wdOutlineView Ni pht hnh: www.giaiphapexcel.com

200

L p trnh VBA for Word


'Checks the state (using the ID in case the tolbar has been customised) of _ all the ShowHeadings buttons on the Outline toolbar; if none are depressed, _ then it must be set to ShowAll. Stores result in a variable so that _ the same level can be returned to at the end of the macro For ShowHeadingLevel = 71 To 77 If CommandBars.FindControl(ID:=ShowHeadingLevel).State Then ShowHeadingLevel = (ShowHeadingLevel - 70) Exit For End If Next ShowHeadingLevel 'if none of the heading level buttons depressed sets variable to 1 If ShowHeadingLevel = 78 Then ShowHeadingLevel = 1 ActiveWindow.View.ShowHeading 3 ActiveDocument.Select WordBasic.ToolsBulletsNumbers Replace:=0, Type:=1, Remove:=1 If ShowHeadingLevel = 0 Then ActiveWindow.View.ShowAllHeadings Else ActiveWindow.View.ShowHeading ShowHeadingLevel End If ActiveWindow.View.Type = ViewType MyRange.Select Set MyRange = Nothing Application.ScreenUpdating = False End Sub

14. I want the numbers in my footnotes not to be superscripted, and I want the numbers to be followed by a dot and a tab
Article contributed by Dave Rado Unfortunately, the same character style (Footnote Reference) is used by the footnote reference and by the number in the footnote itself; and Word offers no way of changing this via the user interface. Many style manuals recommend that in the footnotes themselves, the footnote number should not be superscripted, but that it should be followed by a period (it isn't) and should have a hanging indent 201

Ni pht hnh: www.giaiphapexcel.com

L p trnh VBA for Word (it doesn't). Unsuperscripted endnotes are in even more common use than unsuperscripted footnotes, and again, Word does not cater for them. To get round this, you firstly, you need to redefine the paragraph style Footnote Text such that it has a hanging indent. Select Format + Style + Modify + Format + Paragraph; set the Left Indent to (let's say) -0.25" and the Hanging indent to (let's say) 0.25".On the Modify Style dialog, tick the Add to template checkbox. Click OK, and Close, and then hold the Shift key down and select File + Save All, to ensure that the changes are saved to your template. Having redefined the Footnote Text paragraph style, you can select the number in the footnote every time you insert a footnote; press Ctrl+Spacebar to remove the Footnote Reference character style, and type a dot and a tab immediately after the number. However, it is simpler to fix the problem using a macro. Once you've defined the Footnote Text paragraph style to have a hanging indent, create a macro called InsertFootnote. By giving it that name, it will automatically intercept Word's InsertFootnote command. If you store the macro in a Global template it will be available regardless of which template is in use. Paste the following code into your macro, save your template, and Bob's your Uncle:
Sub InsertFootnote() ActiveDocument.Footnotes.Add Range:=Selection.Range With Selection .Paragraphs(1).Range.Font.Reset .Paragraphs(1).Range.Characters(2) = "" .InsertAfter "." & vbTab .Collapse wdCollapseEnd End With End Sub

If you want to use endnotes, then once you've defined the Endnote Text paragraph style to have a hanging indent, you could use the following variation on the same theme to automatically insert an Endnote formatted correctly, sans superscript, when the Insert + Footnote menu is invoked:
Sub InsertFootnote() ActiveDocument.Endnotes.Add Range:=Selection.Range With Selection .Paragraphs(1).Range.Font.Reset .Paragraphs(1).Range.Characters(2) = "" .InsertAfter "." & vbTab .Collapse wdCollapseEnd End With End Sub Ni pht hnh: www.giaiphapexcel.com

202

L p trnh VBA for Word By storing one or other of these macros in your templates, you can ensure that the default behaviour of the Insert + Footnote command is always appropriate for the type of document in use (and save yourself hours).

15. How to speed up Word Automation by hiding the application


And a few gotchas Article contributed by Daryl Lucas Many people know they can speed execution of Word Automation by turning off screen updating:
Word.Application.ScreenUpdating = False

Many do not know, however, that they can get an even greater speed boost by hiding the application altogether. Here is an example from a Visual Basic client:
Private Sub SayHello() On Error GoTo CATCH Dim oWordApp As Word.Application Set oWordApp = New Word.Application With oWordApp Dim oWordDoc As Word.Document Set oWordDoc = .Documents.Add End With With oWordDoc .Content.Text = "Hello, World!" .SaveAs "Hello" .Close End With Set oWordDoc = Nothing oWordApp.Quit Set oWordApp = Nothing EXITHERE Exit Sub CATCH: Err.Raise Err.Number, Err.Source, Err.Description Resume EXITHERE End Sub

Ni pht hnh: www.giaiphapexcel.com

203

L p trnh VBA for Word In the above example, Word launches but does not appear anywhere on the screen. It does not even show up in the Taskbar. (It does, though, show up in NT's Task Manager, in its list of running processes.) Despite this apparent lack of response, Word is very active and quite capable of doing everything it is told-creating a new document, inserting the message, Hello, World!, saving the file, closing it, and quitting. You can verify this by launching Word the old-fashioned way and opening File1 at the bottom of the File menu after running the sample code. Although it would be difficult to do an exhaustive test, in theory this should work from any Automation client-Visual Basic, Excel, PowerPoint, or any other. The functionality depends on Word, not on the Automation client. But it also works even if you run your Automation code from Word itself. Word does not need to display itself in order to run. In the above code, you could omit the lines that create and use the Word.Application object and replace them with invisibility lines:
Private Sub InvokeStealthMode() On Error GoTo CATCH With Word.Application .Visible = False Dim oWordDoc As Word.Document Set oWordDoc = .Documents.Add End With With oWordDoc .Content.Text = "Hello, World!" .SaveAs "Hello" .Close End With Set oWordDoc = Nothing EXITHERE: On Error Resume Next Word.Application.Visible = True Exit Sub CATCH: Err.Raise Err.Number, Err.Source, Err.Description Resume EXITHERE End Sub

Notice that you want to put the Word.Application.Visible = True line in an error-handler or in a spot where you know it will be run if something goes awry. Do not assume that everything will always go fine. (If you do get stuck with an invisible Word in the middle of a crash, you can launch the Task Manager and kill the WINWORD.EXE process.) How much of a difference does invisibility make? Your mileage may vary, but in the informal testing I've done, I've found that Visible = False makes Word work roughly 15% faster than
Ni pht hnh: www.giaiphapexcel.com

204

L p trnh VBA for Word ScreenUpdating = False does on identical tasks. That's about 1 second for every 7-not a trivial amount if your job runs longer than that. Caveats? Chief among them is that repagination routines don't work when Word is invisible. If you need to update page numbers, you will have to show the application window before doing the update:
With Word.Application .Visible = True oWordDoc.Repaginate .Visible = False End With

See Page X of Y displays or prints as Page 1 of 1, Page 2 of 2 etc. for details on dealing with repagination problems. I hear rumors that you may also need to work with the Range object instead of the Selection object, but I have not tested this nearly enough to give a list because I do almost all of my work with Ranges. If you come across specific gotchas associated with using invisible Selection objects, please email word@mvps.org.

Ni pht hnh: www.giaiphapexcel.com

205

L p trnh VBA for Word

Chapter 12: Working with other objects and collections

1. How to convert the hyperlinks in a document to plain text


Or: How to prevent URLs from being converted to hyperlinks while I type Article contributed by Dave Rado, with acknowledgements to Ibby and Jonathan West If you want to prevent urls being converted to hyperlinks as you type them, select Tools + Autocorrect + Autoformat As You Type, and under Replace as you type, turn off Internet and network paths with hyperlinks. If you want to convert all existing hyperlinks in a document into plain text, you could run the following macro:
Sub GetRidOfHlinks() Dim oHlink As Hyperlink, i As Long For i = ActiveDocument.Hyperlinks.Count To 1 Step -1 ActiveDocument.Hyperlinks(i).Delete Next i End Sub

This does not delete the text, it just converts the hyperlink fields to plain text. The reason that a For Each loop can't be used is that many Word collections, including the Hyperlinks collection, are buggy; if you delete a member of the collection using a For Each loop, Word loses track, and as a result, only every second hyperlink gets deleted if you use For Each. And you have to count from the top to the bottom of the collection, as shown, or Word will lose track. The above code always works well in Word 97, but unfortunately, in Word 2000, if your Hyperlink character style is defined to have bold formatting, you will end up with manual bold formatting applied to your URLs after running the code (another bug). To get round that, you could use the following variation instead, which always works, in Word 97 and above:
Sub GetRidOfHlinksWithoutApplyingBold() Dim oHlink As Hyperlink, i As Long, MyRange As Range For i = ActiveDocument.Hyperlinks.Count To 1 Step -1 With ActiveDocument.Hyperlinks(i) Set MyRange = .Range .Delete MyRange.Font.Reset End With Next i Ni pht hnh: www.giaiphapexcel.com

206

L p trnh VBA for Word


End Sub

On the other hand, if you want to remove the hyperlink fields but still have the text of the URLs formatted with the Hyperlink character style, you could use the following variation on the same theme:
Sub GetRidOfHlinksButPreserveCharacterStyle() Dim oHlink As Hyperlink, i As Long, MyRange As Range For i = ActiveDocument.Hyperlinks.Count To 1 Step -1 With ActiveDocument.Hyperlinks(i) Set MyRange = .Range .Delete MyRange.Style = wdStyleHyperlink End With Next i End Sub

Preserving hyperlinks within your Table of Contents


In Word 2000 and above, if your table of contents contains the /h switch (in other words, if the text in the Table of Contents not only the page numbers hyperlinks to your headings), the above code will remove those text hyperlinks as well (although they will be automatically regenerated when you next update the table of contents). That is, it won't unlink the Table of Contents, and the page numbers will still hyperlink to the headings, but the text in the TOC won't hyperlink to the Headings after running the above code. To prevent that from happening, you could use the following code instead:
Sub GetRidOfHlinksExceptInToc() Dim oHlink As Hyperlink, i As Long, MyRange As Range, _ oToc As TableOfContents, LinkIsInToc As Boolean For i = ActiveDocument.Hyperlinks.Count To 1 Step -1 With ActiveDocument.Hyperlinks(i) Set MyRange = .Range LinkIsInToc = False For Each oToc In ActiveDocument.TablesOfContents If MyRange.InRange(oToc.Range) Then LinkIsInToc = True Exit For End If Next oToc If Not LinkIsInToc Then .Delete Ni pht hnh: www.giaiphapexcel.com

207

L p trnh VBA for Word


MyRange.Font.Reset 'or use MyRange.Style = wdStyleHyperlink if you prefer End If End With Next i End Sub

As an aside, if you have no other fields in the document, you can convert all the hyperlinks to plain text manually by Selecting All and pressing Ctrl+Shift+F9 (unlink fields). That's a big if, though, and it's a risk I'd rather not take. The above macros do run very fast.

2. Size the text in a textbox to fill the textbox


Article contributed by Bill Coan
Sub ResizeTextToFitTextBox() If Selection.StoryType <> wdTextFrameStory Then Exit Sub Dim myTextRange As Range Dim myShape As Shape Set myShape = Selection.ShapeRange(1) Set myTextRange = myShape.TextFrame.TextRange myTextRange.Font.Size = 2 If myShape.TextFrame.Overflowing = True Then ActiveDocument.Undo MsgBox "Even when set to a size of 2 points, the text overflows the textbox." Exit Sub End If Do Until myShape.TextFrame.Overflowing = True myTextRange.Font.Size = _ myTextRange.Font.Size + 0.5 Loop myTextRange.Font.Size = _ myTextRange.Font.Size - 0.5 End Sub

Ni pht hnh: www.giaiphapexcel.com

208

L p trnh VBA for Word

3. When I position a floating object (such as a text box or graphic) Relative to Page in Word 2000, it doesnt end up where it should why doesnt it?
Article contributed by Dave Rado Word 2000 can be a nightmare when it comes to positioning shapes Word 97 was much more predictable. The problem in Word 2000 is that:

Anchors can be within a table If the anchor is within a table you can't position the shape relative to the page (if you select that option, it gets positioned relative to the table instead!) Anchors in Word 2000 have a way of jumping into tables when you're looking the other way, and then laughing at you while you try to figure out what happened!

So if you're positioning shapes manually, lock the anchor to a paragraph that is not in a table before you start. If you're doing it in code, anchor the shape to a paragraph that is not in a table, and lock the anchor. If you want a shape (such as a line, text box or graphic) to appear on every page, you should put it in the Header and set it to be behind text. The final paragraph in the Header cannot be in a table, so it makes sense to anchor it to that paragraph Here's an example of how to do it with code (you could use the same principle for watermarks). This code inserts two lines in the primary Header of the first section of the document:
Sub AddLinesToHeader() Dim oHeader As HeaderFooter, MyRange As Range, oLine As Shape System.Cursor = wdCursorWait Set oHeader = ActiveDocument.Sections(1).Headers(wdHeaderFooterPrimary) 'Make sure the shape anchored to the final paragraph in the Header Set MyRange = oHeader.Range.Paragraphs.Last.Range Set oLine = oHeader.Shapes.AddLine(0, 0, 0, 0, Anchor:=MyRange) With oLine .RelativeHorizontalPosition = wdRelativeHorizontalPositionPage .RelativeVerticalPosition = wdRelativeVerticalPositionPage .Width = CentimetersToPoints(1) .Left = CentimetersToPoints(0.5) .Top = CentimetersToPoints(21.76) .LockAnchor = True .WrapFormat.Type = wdWrapNone 'It's a good idea to name shapes you add, so that if you want to hide them, 'or whatever later, you can refer to them by name Ni pht hnh: www.giaiphapexcel.com

209

L p trnh VBA for Word


.Name = "First Line" 'Add any other parameters you need, eg .Line.Weight = etc End With Set oLine = oHeader.Shapes.AddLine(0, 0, 0, 0, Anchor:=MyRange) With oLine .RelativeHorizontalPosition = wdRelativeHorizontalPositionPage .RelativeVerticalPosition = wdRelativeVerticalPositionPage .Width = CentimetersToPoints(1) .Left = CentimetersToPoints(0.5) .Top = CentimetersToPoints(22.08) .LockAnchor = True .WrapFormat.Type = wdWrapNone .Name = "Second Line" 'add other parameters you need, eg .Line.Weight = etc End With System.Cursor = wdCursorNormal End Sub

4. Move shape anchors away from heading paragraphs


Article contributed by Bill Coan
Problem

When shape anchors are located in heading paragraphs, the table of contents is unable to display heading numbers. This routine works even on shapes whose anchors are locked. It preserves the location of a shape even if the shape is positioned relative to paragraph! Solution Cut offending shapes out of the document and paste them back into the document with their anchors at a new location, immediately below the heading paragraph. Do this without affecting the location of the shape on the page.
Sub MoveAnchorsOutOfHeadings() Dim oShape As Shape For Each oShape In ActiveDocument.Shapes If Left$(oShape.Anchor.Style, 7) = "Heading" Then oShape.Select Selection.Cut Selection.MoveDown unit:=wdParagraph, Count:=1 Selection.Paste End If Next oShape End Sub Ni pht hnh: www.giaiphapexcel.com

210

L p trnh VBA for Word

5. The simplest way, using VBA, to reset part of a style definition (e.g. the font name), so it inherits the definition of the style it is based on
Article contributed by Henk van Boeijen In Word, styles can be (and usually are) based on other styles. If you create a new style, Word bases it on the style of the paragraph that is currently selected, unless you explicitly change this in the Modify Style dialog. The style it is based on is called its base style (or sometimes, the parent style). 1. If it's a paragraph style, the new style will inherit (in an object-oriented fashion) all of its font and paragraph properties from its base style, and will be defined by Word only as the definition of the base style plus anything you have defined to be different from the base style. So in the Format + Style dialog, you will see definitions like: Normal + Indent: Hanging 0.25", Space After 6 point. In this example, only the Indent and the Space After setting are held in the style definition. So if you change the Font in the Normal style, the font in the child style will automatically change, too. But if you change the Space After property of the Normal style, the Space After property of the child style will not change, because that property is stored in the child style's definition and so is not inherited. Similarly, if you see a definition like: Normal + Font Arial in the Format + Style dialog, then nothing but the font name is stored in the style definition. So if you change the Font of your Normal style, the font in the child style will not change; but if you change the Space After property of the Normal style, the Space After property of the child style will change. 2. Character styles behave similarly except that only font properties are defined or inherited. So you will see character style definitions in the Format + Style dialog like Default Paragraph Font + Font color: Dark Blue. In this example, nothing is stored in the character style definition except the font colour. Everything else is inherited from the underlying paragraph style of whichever paragraph you happen to have applied the character style to. So a style can be based on another style which in turn is based on another style, and so on. In this way you can form an hierarchy tree of styles. Each member in that tree inherits the formatting options of its parent and adds its own specific options to it. Used sensibly, this mechanism is absolutely essential to your ability to maintain complex documents and templates. Another example
Style "Normal": - Font.Name = "Times New Roman" - Font.Size = 11 - Font.Bold = False Ni pht hnh: www.giaiphapexcel.com

211

L p trnh VBA for Word


Style "Special": - this style is based on "Normal" - Font.Size = 9 Style "MoreSpecial" - this style is based on "Special" - Font.Bold = True

All three styles share the same font name. The font name is defined in Normal and is inherited by Special and MoreSpecial. When the font name in Normal is changed to Arial, this change also applies to Special and MoreSpecial. The font size is only shared by Special and MoreSpecial: 9 points is defined in Special. It overrides the font size setting of Normal which is 11 points. MoreSpecial inherits 9 points from Special. If the font size of Normal is changed to 12 points, this setting will not be inherited by Special or MoreSpecial. In some cases you may want to delete formatting properties in a style's definition, in order that it should inherit those properties from its parent (or base) style. To do this in VBA, use the following logic:
Dim oStyle As Style Set oStyle = ActiveDocument.Styles("Special") oStyle.Font.Name = oStyle.BaseStyle.Font.Name

After running this code, the font name will no longer be stored in the style definition of the Special style, but will now be inherited from the Normal style. If you need to do this more than once, you could make your life easier by calling a subroutine like the following, which clears the font name of a given style. It accepts a style name (of type String) or a WdBuiltinStyle constant, or a style object, as its argument. So you could call it like this:
ClearStyle "List Number"

or like this:
ClearStyle wdStyleListNumber

or like this:
ClearStyle ActiveDocument.Styles("List number")

If the style is not based on another style, the routine exits without generating an error.
Public Sub ClearStyleFont(oStyle As Variant) Ni pht hnh: www.giaiphapexcel.com

212

L p trnh VBA for Word


Dim oBaseFont As Font With ActiveDocument.Styles(oStyle) If .BaseStyle = "" Then 'There is no base style, nothing to clear Else Set oBaseFont = .BaseStyle.Font With .Font .Name = oBaseFont.Name 'Add other font properties here if needed End With Set oBaseFont = Nothing End If End With End Sub

You could use exactly the same logic to clear paragraph properties from a style definition, so that they are inherited from the base style. If you wanted to clear all font and paragraph properties from a style definition, (including any list numbering, language and borders definitions, although unfortunately, not including Frames definitions), in order that you can start defining your own properties for the style knowing that you are starting with a clean slate, you could use the following subroutine.
Public Sub ClearStyle(oStyle As Variant) With ActiveDocument.Styles(oStyle) If .BaseStyle <> "" Then .Font = .BaseStyle.Font .ParagraphFormat = .BaseStyle.ParagraphFormat End If End With End Sub

6. Cycle a paragraph through all available paragraph styles, eventually returning to the style the paragraph started with
Article contributed by Bill Coan The following macro includes a line that prevents execution when text is selected. (The cursor must be flashing for the macro to run.) I played with this a bit because someone else thought it would be a nice feature. I assigned the macro to a button, then to a keystroke. I found the keystroke much easier to use. But Word has so many built-in styles that it can be tedious to keep cycling through the styles until the original style comes back around.
Ni pht hnh: www.giaiphapexcel.com

213

L p trnh VBA for Word


Sub CycleThroughStyles() Dim NeedToRollOver As Boolean Dim i As Long, j As Long, k As Long NeedToRollOver = True 'quit if cursor isn't flashing. 'This limits action to one paragraph If Selection.Type <> wdSelectionIP Then GoTo EndGracefully 'find the current paragraph style, then 'find the next available paragraph style For i = 1 To ActiveDocument.Styles.Count If Selection.Paragraphs(1).Style = ActiveDocument.Styles(i) Then For j = i + 1 To ActiveDocument.Styles.Count If ActiveDocument.Styles(j).Type = wdStyleTypeParagraph Then Selection.Paragraphs(1).Style = ActiveDocument.Styles(j) NeedToRollOver = False Exit For End If Next j End If If NeedToRollOver = False Then Exit For End If Next i

'if we reached the last paragraph style, then 'roll over to first available paragraph style If NeedToRollOver = False Then GoTo EndGracefully For k = 1 To ActiveDocument.Styles.Count If ActiveDocument.Styles(k).Type = wdStyleTypeParagraph Then Selection.Paragraphs(1).Style = ActiveDocument.Styles(k) Exit For End If Next k 'tell user what current style is. 'clear the undo buffer to prevent error message 'about document formatting being too complex. EndGracefully: Application.StatusBar = Selection.Paragraphs(1).Style ActiveDocument.UndoClear Ni pht hnh: www.giaiphapexcel.com

214

L p trnh VBA for Word


End Sub

7. How to safely update a document's styles from its template without using the Organizer (and how to make the Tools + Templates and Add-ins dialog safe)
Article contributed by Dave Rado, Margaret Aldis, Ian Sharpe and Beth Melton

7.1. Overview of updating styles and template strategy


If you want to update the style definitions of a document with the style definitions in its attached template, you can manually select Tools + Templates and Add-ins, check the box which says Automatically update document styles, click OK; and then, because that setting is sticky (and most of the time, undesirable), immediately select Tools + Templates and Add-ins again, deselect the Automatically update document styles box, and click OK. If the attached template is Normal.dot, this doesn't work in Word 97; but it works for all other attached templates in Word 97; and it works for all templates, including Normal.dot, in later versions of Word. (It can be made to work in Word 97, even if the attached template is Normal.dot, with a little programming this is covered below). It is important to realise that the only styles that are updated when you update a document's styles from its template are the ones listed in the template itself under Styles in use (in the Style dialog, where it says List). So if a user modifies the definition of a built-in style in their document, but that style was not listed under Styles in use in the template, then updating their styles from the template will have no affect on that particular style definition. Because of this, if you are a template designer, you must ensure that any built-in styles which you want control over are listed under Style in use. The best way to add a built-in style to the In use list is to insert it in the template (first redefining it if required). You could use a dummy paragraph for the purpose, and delete it again once you have finished applying your styles. The styles, having been applied once, will remain in the In use list for ever more.

7.2. When should styles be updated?


The Automatically update document styles setting is document-specific, and if left switched on when a particular document is saved, will update that document's styles whenever it is opened by anyone (until the document is saved again with the setting switched off). This is usually undesirable, because, whereas one would want a document's styles to be updated whenever a style in its template is redefined while a document is being revised, once the document is live, one would definitely not. Also if emailing to an external reviewer who does not have access to the template, the setting should emphatically not be switched on.
Ni pht hnh: www.giaiphapexcel.com

215

L p trnh VBA for Word So in a corporate context, it's best never to save a document with its Automatically update document styles setting switched on. Instead, assuming you have a version control strategy, store the draft/live status of a document in a document variable (or document property); and by using an AutoOpen macro to read the variable or property, update the styles if it's in Draft mode, but not if it's Live. In addition, your users may want to update a document's styles using the dialog, if their style definitions have been mangled. Updating a document's styles from its template is a lot faster than using the Organizer (and subject to the qualifications below, seems to be more reliable, as well).

7.3. Gotchas to be aware of, and their workarounds


As has already been mentioned, updating styles from the template doesn't work in Word 97 if the attached template is Normal.dot. More seriously, in all versions of Word, updating styles can sometimes mangle your style definitions, especially in the case of numbering styles, where it can sometimes destroy the link between the styles and their List Templates. But the good news is that updating styles can be made reliable, as follows. 1. For it to be reliable, your numbering styles must use named List Templates. See the links at How to cure Word's List Numbering with a dose of VBA for more details. 2. All the styles in the Styles in Use list need to have been physically applied to text in the template once (but they do not need to have been applied in documents based on the template). What the difference is, in terms of the flags stored inside a template file, between a style that is listed in the In use list but has never been applied to text, and one that has been applied, is a mystery; but it seems there is a difference. Unfortunately, it is all too easy to add a style to the Styles in Use list without ever applying the style to any text. In the case of built-in styles, you can do this manually by selecting Format + Style, where it says List, select All styles, and click Modify + OK + Close. Or programmatically, you can add it to the In use list by running code that defines the style. In the case of custom styles, you can add them to the In use list without applying them to text by selecting Format + Style + New + OK + Close; or programmatically, by defining a new style without applying it. Styles (especially numbering styles) that are listed in the In use list of a template, but have never physically been applied to text, will not be stable if you update your styles from the template. So if in doubt, it is a good idea to insert a dummy paragraph in each of your templates, cycle it through all the styles in the template's In use list, delete the dummy paragraph, and save the template. And if you subsequently need to redefine any of the styles in the template, it is again a good idea to apply the redefined style to a dummy paragraph. 3. Sometimes, you have to do the update twice or occasionally even three times in order preserve your List Template names and their links to your numbering styles:
Dim oLT As ListTemplate Ni pht hnh: www.giaiphapexcel.com

216

L p trnh VBA for Word


ActiveDocument.UpdateStyles ActiveDocument.UpdateStyles On Error Resume Next For Each oLT In ActiveDocument.AttachedTemplate.ListTemplates If Not oLT.Name = "" Then If Not ActiveDocument.ListTemplates(oLT.Name).ListLevels(1) _ .LinkedStyle = oLT.ListLevels(1).LinkedStyle Then ActiveDocument.UpdateStyles Exit For End If End If Next oLT

No theories as to why this might be; it's a bug; but at least there is a workaround. You can make the Tools + Templates and Add-ins dialog safe for users to use by intercepting the Word command as follows:
Sub FileTemplates() With Dialogs(wdDialogToolsTemplates) .Show If .LinkStyles = 1 Then ActiveDocument.UpdateStyles Dim oLT As ListTemplate On Error Resume Next For Each oLT In ActiveDocument.AttachedTemplate.ListTemplates If Not oLT.Name = "" Then If Not ActiveDocument.ListTemplates(oLT.Name).ListLevels(1) _ .LinkedStyle = oLT.ListLevels(1).LinkedStyle Then ActiveDocument.UpdateStyles Exit For End If End If Next oLT ActiveDocument.UpdateStylesOnOpen = False End If End With End Sub

[The ActiveDocument.UpdateStylesOnOpen = False line in the above code sample prevents the user from being able to save the document with the Automatically update document styles setting switched on; and means they don't have to immediately go back to the dialog every time they update their styles, simply in order to deselect that setting. If that's not what you want, though, you can remove that line. But if you do remove that line, then you will also need to have an AutoOpen macro that checks whether the setting is switched on, and if it is, that updates the styles again if need be; otherwise the numbering styles will sometimes be broken when the
Ni pht hnh: www.giaiphapexcel.com

217

L p trnh VBA for Word document is reopened.] But unfortunately, the Dialogs(wdDialogToolsTemplates) object is buggy the dialog it displays doesn't show the list of add-ins, and half the buttons on it are greyed out. This is well worth emailing mswish@microsoft.com about. You can get round this as follows: a) Instead of intercepting the FileTemplates command, create the following macro:
Sub ReplacementToolsTemplatesAndAddins() 'Execute the built-in button CommandBars.FindControl(ID:=751).Execute If Dialogs(wdDialogToolsTemplates).LinkStyles = 1 Then Dim oDoc As Document, oLT As ListTemplate Set oDoc = ActiveDocument oDoc.UpdateStyles On Error Resume Next For Each oLT In oDoc.AttachedTemplate.ListTemplates If Not oLT.Name = "" Then If Not oDoc.ListTemplates(oLT.Name).ListLevels(1) _ .LinkedStyle = oLT.ListLevels(1).LinkedStyle Then oDoc.UpdateStyles Exit For End If End If Next oLT oDoc.UpdateStylesOnOpen = False End If End Sub

(Again, remove the ActiveDocument.UpdateStylesOnOpen = False line if you don't want it.) b) Create a new toolbar, name it Hidden, and move the built-in Templates and Add-ins button to the new toolbar. This is in order that the above macro can execute the built-in button (doing so avoids the bugs that you get if you use Dialogs(wdDialogToolsTemplates)), without the built-in button being visible to the user. c) Go to Tools + Customize, select the Commands tab; in the left pane, select All macros, in the right pane select the ReplacementToolsTemplatesAndAddins macro, and drag it onto the Tools menu, to where the Templates and Add-ins button used to be. Right-click the new button and rename it: Templates and Add-&Ins... d) Disable the new toolbar (which makes it invisible to the user), as follows:
CommandBars("Hidden").Enabled = False

e) If you've made the above customisations in an add-in, create an AutoExec macro as follows:
Sub AutoExec() Ni pht hnh: www.giaiphapexcel.com

218

L p trnh VBA for Word


CommandBars("Hidden").Enabled = False End Sub

Or if you've customized a template rather than an add-in, create an AutoNew and AutoOpen macro in the template, containing the same code. If you have already disabled the Web toolbar, of course, one could use that for this purpose, rather than creating a new Toolbar called Hidden. See also: How to stop the web toolbar from jumping up at you whenever you click on a page number in the table of contents. With one qualification, your users should now be able to update their styles from their template(s) safely. That one qualification is this: the above macro will safely update styles if their definitions have been changed in the attached template, and will safely revert them to the attached template's definitions if they have been redefined in the document. It will also safely update styles that are defined in the attached template but not in the document. But if there are broken list numbering styles in the document, it will sometimes, but not always, fix those. In other words, it is not a complete substitute for a Fix numbering macro (although it should greatly reduce the frequency with which you'll need to run the latter). For details of the latter, see the links at How to cure Word's List Numbering with a dose of VBA.

If you want to be able to update the styles of Word 97 documents that are attached to Normal.dot
If instead of using the UpdateStyles method, you use CopyStylesFromTemplate, then you can update your document's styles even in Word 97, when the attached template is Normal.dot. However, using CopyStylesFromTemplate is significantly slower than using UpdateStyles; so the following variation on the above macro only uses the slower method when necessary, and uses UpdateStyles when possible:
Sub ReplacementToolsTemplatesAndAddins() Dim Word97Normal As Boolean, oDoc As Document, oLT As ListTemplate 'Execute the built-in button CommandBars.FindControl(ID:=751).Execute If Dialogs(wdDialogToolsTemplates).LinkStyles = 1 Then Set oDoc = ActiveDocument If Left$(Application.Version, 1) = "8" And _ oDoc.AttachedTemplate = NormalTemplate Then Word97Normal = True End If If Word97Normal Then oDoc.CopyStylesFromTemplate NormalTemplate oDoc.CopyStylesFromTemplate NormalTemplate Else Ni pht hnh: www.giaiphapexcel.com

219

L p trnh VBA for Word


oDoc.UpdateStyles End If On Error Resume Next For Each oLT In oDoc.AttachedTemplate.ListTemplates If Not oLT.Name = "" Then If Not oDoc.ListTemplates(oLT.Name).ListLevels(1) _ .LinkedStyle = oLT.ListLevels(1).LinkedStyle Then If Word97Normal Then oDoc.CopyStylesFromTemplate NormalTemplate Else oDoc.UpdateStyles End If Exit For End If End If Next oLT oDoc.UpdateStylesOnOpen = False End If End Sub

8. Scroll all open documents the same percentage as the active document
Article contributed by Bill Coan Scroll the active document to the desired point, then run a macro that scrolls all other open documents to the same percentage. The following code is written for Word 97. It looks at how far you've scrolled the active window, then scrolls all other document windows the same percentage. Of course, if one document is 10 pages long and another is 100 pages long, then a 50% vertical scroll would put you on page 5 in one document and page 50 in the other.
Sub ScrollAllWindowsALike() Dim myWindow As Window Set myWindow = ActiveWindow ScrollPercent = myWindow.VerticalPercentScrolled For Each oWindow In Application.Windows oWindow.Activate oWindow.VerticalPercentScrolled = ScrollPercent Next oWindow myWindow.Activate End Sub Ni pht hnh: www.giaiphapexcel.com

220

L p trnh VBA for Word

Ni pht hnh: www.giaiphapexcel.com

221