Maximising the performance of Word tables

Article contributed by Dave Rado

  1. As a user
  2. In code

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:


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

  

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 text-wrapping 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.

2–7 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
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 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()

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

Word 2000

OperateOnEveryCellUsingTableObject

38.5s

53.8s

OperateOnEveryCellInSelectedTable

5.3s

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 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 contacting http://support.microsoft.com/contactus/?ws=support 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 contacting http://support.microsoft.com/contactus/?ws=support.

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
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

Word 2000

OperateOnColumn3UsingRanges

7.3s

10.7s

OperateOnSelectedColumn3

0.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. 

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.