RichEdit 8.0 TOM Table Interfaces

An earlier post describes the RichEdit nested table facility and how the EM_INSERTTABLE and EM_GETTABLEPARMS messages could be used to insert and examine tables. Now those messages are documented in MSDN along with a new message, EM_SETTABLEPARMS that allows one to modify tables. For additional convenience, RichEdit 8.0 adds table support to the TOM text object model. The APIs are all documented in MSDN, but it’s worthwhile to give an overview here to help motivate the approach.

If you’d just like to insert a table with any number of identical rows and default properties, call ITextRange2::InsertTable(). To insert more complicated kinds of tables, RichEdit 8.0 has the ITextRow table interface. In addition to inserting tables, this interface allows you to examine tables and to perform table manipulations, such as inserting, deleting and resizing table columns. The interface is associated with an ITextRange2 and hence doesn’t depend on (and change) the selection, which is used by the table messages, EM_INSERTTABLE, EM_GETTABLEPARMS, and EM_SETTABLEPARMS.

To obtain an ITextRow interface, call ITextRange2::GetRow(). To insert one or more identical table rows, call ITextRow::Insert(). To insert nonidentical rows, call ITextRow::Insert() for each different row configuration. This allows you to have tables consisting of rows with mixtures of cell counts, row indents and other properties. In particular, tables don’t have to be rectangular.

To select a table, row, or cell, use ITextRange::Expand(), with the Unit tomTable, tomRow, and tomCell, respectively. These Units can also be used with the ITextRange::Move() methods to navigate and select multiple rows or cells. Although having only two interfaces for handling tables may seem sparse compared to extensive table models such as Microsoft Word’s, there’s a nice simplicity to the approach. Instead of dealing with interfaces for collections of cells, rows, columns and tables, along with individual cell, row, column and table interfaces, you only have to learn and use two interfaces. Nevertheless ITextRow and ITextRange2 harness the complete power of RichEdit’s nested table facility.

Some ITextRow properties apply to a whole row, such as the row alignment. In addition there are cell properties, such as cell alignment. Cell properties are applied to the active cell, which is the one selected via the ITextRow::SetCellIndex() method. To set cell properties on different cells, change the active cell in between property set calls. The use of an active cell along with the ITextRange navigation methods obviate the need for a cells collection interface.

ITextRow works similarly to ITextPara2, but does not modify the text in the associated range until either the ITextRow::Apply or the ITextRow::Insert method is called. In addition, the row and cell parameters are always active, that is, they cannot have the value tomDefault. On initialization, the ITextRow object acquires the table row properties (if any) at the active end of the associated ITextRange2. The ITextRow::Reset method can be used to update these properties to the current values for ITextRange2.

When the ITextRow::Apply method is given the tomCellStructureChangeOnly flag, only the number of cells and/or the cell widths are changed. Other properties remain the same. This is handy for deleting, inserting or resizing table columns. The EM_SETTABLEPARMS message also has this capability.

RichEdit tables are stored quite efficiently. An empty cell consists of a single cell mark (U+0007) plus the cell property information. The latter can be compressed a bit by allowing trailing cells to share the same set of properties. This is done via the ITextRow::SetCellCountCache(). In contrast to the ITextRow::SetCellCount() method, which sets the number of cells in a row, the SetCellCountCache() method sets the number of cells with cached parameters. Cells that follow the last cached cell use the same properties as the last cached cell. For example, the call ITextRow::SetCellCountCache(1) causes all cells in the row to use the same set of properties.

The architecture is quite flexible in that each table row can have any valid table-row parameters regardless of the parameters for other rows (except for vertical merge flags). For example the number of cells and the start indents of table rows can differ, unlike in HTML which has n×m rectangular format with all rows starting at the same indent.

On the other hand, no formal table description is stored anywhere. Information such as the table row count has to be figured out by navigating through the table. One way to obtain this count is to call ITextRange::StartOf (tomTable, tomFalse, NULL) to move to the start of the current table and then to call ITextRange::Move (tomRow, tomForward, &dcRow). On return, the table row count is given by dcRow + 1, since moving by tomRow’s doesn’t move beyond the last table row.

Comments (1)

  1. Rob S says:

    Is it possible to add a row to the end of an existing table?

    I've tried using ITextRow.Insert:

    – getting the range of the last table row by setting up a range to the row start marker (uFFF9) of the last row, calling GetRow, then Insert. This inserts the new row above the last row.

    I then tried positioning the range on the last row end marker (uFFFB), calling GetRow then Insert. This adds the row after the last row, but it seems to behave like a separate table because range.expand(table) only includes the newly added row (also dragging column widths in the rtf control only updates the added row).

    I'm using the .net richTextControl (winforms) with class RICHEDIT60W, referencing riched20.dll from the Office2013 install.

    Any insight on how to do this is greatly appreciated, as is this entire blog!