RichEdit’s Nested Table Facility


One subject that seems to come up every other month or so is how RichEdit tables work. So I might as well post the answer. Hopefully RichEdit tables will eventually be described in the Windows SDK. They are not directly related to Math in Office, but I had mathematical expressions in mind when designing RichEdit’s table facility. Both mathematics and tables are recursive. For example you can have a fraction in the numerator of another fraction, and you can have a table in the cell of another table. So implementing tables seemed like a useful project that might also reveal how to implement a WYSIWYG implementation of mathematics. In fact, MathML <mtable>’s have a lot in common with general tables.


 


Most people at the time (1999) were recommending that a table cell should be represented by a whole RichEdit instance, which would give great generality. But I wanted a model that was much smaller, faster and worked with the built-in Find/Replace functionality and the RTF file converters. To this end, we needed a model, like Word’s, that was part of a single document instance, and could be overlaid on the existing paragraph structure. Accordingly RichEdit’s table implementation is very efficient and fast, in fact, much faster than Word’s (although less general). Improvements have been made over the years, but the discussion that follows applies to RichEdit 4.0, which shipped with Office 2002, and RichEdit 4.1, which ships with Windows XP and Vista to this day. It also applies to later versions that ship with Office 2003 & 2007, which have additional features..


 


Specifically a cell containing a single line of text is represented only by that text, not by some larger structure. An empty cell consists of the single character, the cell mark U+0007. A cell containing multiple lines of text is expressed in terms of a structure that is substantially smaller than a complete edit instance, followed by the CELL mark. Tables can be nested up to 15 levels deep; higher nestings are represented by tab-delimited text. Cells can contain multiple paragraphs of any kind, e.g., bidirectional text, arbitrary tabs and alignments.


 


The Spring of 1999 was shortly after the Unicode Technical Committee added the U+FFF9..U+FFFB delimiter characters for describing ruby text in Japanese. These characters were available for more general use and seemed ideal for RichEdit’s internal table structure. This choice preceded the addition of the internal-use-only U+FDDO..U+FDEF characters that we use for mathematical structure characters, among other things.


 


In the (in-memory) backing store, a table row has the form


 


    {CR…}CR


 


where { stands for the Unicode STARTGROUP character U+FFF9, and CR  is the ASCII Carriage Return character U+000D. The delimiter } stands for the Unicode ENDGROUP character U+FFFB and … stands for a sequence of cells, each consisting of cell text terminated by the CELL mark U+0007. For example, a row with three empty cells has the plain text understructure U+FFF9 U+000D U+0007 U+0007 U+0007 U+FFFB U+000D. The start and end group character pairs are assigned identical PARAFORMAT2 information that describe the row and cell parameters.  If rows with different parameters are needed, they may follow one another with appropriate PARAFORMAT2 parameters. A horizontally or vertically merged cell has two characters: NOTACHAR (0xFFFF) followed by CELL (0x7). Any text that appears in a merged cell is stored in the first cell of the set of merged cells.


 


One way to insert tables is to copy/paste tables from Word. RichEdit reads and writes table RTF. For more programmatic purposes, RichEdit 4.0 introduced the message EM_INSERTTABLE (WM_USER + 232), which acts similarly to EM_REPLACESEL but inserts one or more table rows with empty cells instead of plain text. Specifically it deletes the text (if any) currently selected by the selection and then inserts empty table row(s) with the row and cell parameters given by wparam and lparam, respectively, as defined below. It leaves the selection pointing to the start of the first cell in the first row. The client can then populate the table cells by pointing the selection at the various cell end marks and inserting and formatting the desired text. Such text can include nested table rows, etc. Since wparam and lparam point at row and cell parameter structures, this API isn’t compatible with Visual Basic and can’t be easily added to RichEdit’s object model TOM, although TOM2 does have a general set of table interfaces.


 


The TABLEROWPARMS and TABLECELLPARMS structures are defined as


 


typedef struct _tableRowParms


{                           // EM_INSERTTABLE wparam is a (TABLEROWPARMS *)


    BYTE    cbRow;          // Count of bytes in this structure


    BYTE    cbCell;         // Count of bytes in TABLECELLPARMS


    BYTE    cCell;          // Count of cells


    BYTE    cRow;           // Count of rows


    LONG    dxCellMargin;   // Cell left/right margin (\trgaph)


    LONG    dxIndent;       // Row left (right if fRTL indent (similar to \trleft)


    LONG    dyHeight;       // Row height (\trrh)


    DWORD   nAlignment:3;   // Row alignment (like PARAFORMAT::bAlignment,


                            //  \trql, trqr, \trqc)


    DWORD   fRTL:1;         // Display cells in RTL order (\rtlrow)


    DWORD   fKeep:1;        // Keep row together (\trkeep}


    DWORD   fKeepFollow:1;  // Keep row on same page as following row (\trkeepfollow)


    DWORD   fWrap:1;        // Wrap text to right/left (depending on bAlignment)


                            // (see \tdfrmtxtLeftN, \tdfrmtxtRightN)


    DWORD   fIdentCells:1;  // lparam points at single struct valid for all cells


} TABLEROWPARMS;


 


typedef struct _tableCellParms


{                           // EM_INSERTTABLE lparam is a (TABLECELLPARMS *)


    LONG    dxWidth;        // Cell width (\cellx)


    WORD    nVertAlign:2;   // Vertical alignment (0/1/2 = top/center/bottom


                            //  \clvertalt (def), \clvertalc, \clvertalb)


    WORD    fMergeTop:1;    // Top cell for vertical merge (\clvmgf)


    WORD    fMergePrev:1;   // Merge with cell above (\clvmrg)


    WORD    fVertical:1;    // Display text top to bottom, right to left (\cltxtbrlv)


    WORD    wShading;       // Shading in .01% (\clshdng) e.g., 10000 flips fore/back


 


    SHORT   dxBrdrLeft;     // Left border width (\clbrdrl\brdrwN) (in twips)


    SHORT   dyBrdrTop;      // Top border width  (\clbrdrt\brdrwN)


    SHORT   dxBrdrRight;    // Right border width (\clbrdrr\brdrwN)


    SHORT   dyBrdrBottom;   // Bottom border width (\clbrdrb\brdrwN)


    COLORREF crBrdrLeft;    // Left border color (\clbrdrl\brdrcf)


    COLORREF crBrdrTop;     // Top border color (\clbrdrt\brdrcf)


    COLORREF crBrdrRight;   // Right border color (\clbrdrr\brdrcf)


    COLORREF crBrdrBottom;  // Bottom border color (\clbrdrb\brdrcf)


    COLORREF crBackPat;     // Background color (\clcbpat)


    COLORREF crForePat;     // Foreground color (\clcfpat)


} TABLECELLPARMS;


 


Note that paragraph-format information containing the TABLEROWPARMS and TABLECELLPARMS information is attached to the table-row delimiters as set up by the EM_ INSERTTABLE message, so merely duplicating the plain-text table structure in the backing store isn’t enough to insert a working table. In fact, methods like ITextRange::SetText() convert the special delimiters U+FFF9.U+FFFB to spaces (U+0020). Note also that this table structure is nestable.


 


The definition of EM_INSERTTABLE is extensible, since in the future we’ll probably have to support more parameters for table rows and cells. The API also inserts a consistent table row all at once, so that no illegal table parts are present on return. Hence if the document is saved after such an insertion, valid Word-compatible RTF will be written. lparam points at the TABLECELLPARMS structure for the first cell in an array of TABLECELLPARMS structures.  It’s important that cbCell = sizeof(TABLECELLPARMS).  That way RichEdit knows how much cell information the client is specifying.  In particular, in the future if more cell parameters are defined, older clients can get away with specifying less and the new RichEdit can assign default values for the new parameters.  Similarly cbRow says how many bytes are defined by the client for TABLEROWPARMS, in case RichEdit is revised to support more row parameters that the client doesn’t know about.


 


To make simple tables easier to define, if fIdenticalCells = 1, lparam points at a single TABLECELLPARMS structure that is valid for all cells in the row.  Note that a nonzero cell border width is guaranteed to give at least a one-pixel border.


 


The colors are limited to the standard 16 colors defined by


 


      RGB(  0,   0,   0),     // \red0\green0\blue0


      RGB(  0,   0, 255),     // \red0\green0\blue255


      RGB(  0, 255, 255),     // \red0\green255\blue255


      RGB(  0, 255,   0),     // \red0\green255\blue0


      RGB(255,   0, 255),     // \red255\green0\blue255


      RGB(255,   0,   0),     // \red255\green0\blue0


      RGB(255, 255,   0),     // \red255\green255\blue0


      RGB(255, 255, 255),     // \red255\green255\blue255


      RGB(  0,   0, 128),     // \red0\green0\blue128


      RGB(  0, 128, 128),     // \red0\green128\blue128


      RGB(  0, 128,   0),     // \red0\green128\blue0


      RGB(128,   0, 128),     // \red128\green0\blue128


      RGB(128,   0,   0),     // \red128\green0\blue0


      RGB(128, 128,   0),     // \red128\green128\blue0


      RGB(128, 128, 128),     // \red128\green128\blue128


      RGB(192, 192, 192),     // \red192\green192\blue192


 


plus two custom colors. The border widths are limited to the range 0 to 255 twips.


 


If the color index is not in the range 1..18, then autocolor is used, which usually ends up being the system Text or Background colors.


Comments (20)

  1. David Kinder says:

    Could you also give the numeric value for EM_INSERTTABLEROW? It’s not in the Platform SDK as far as I can see.

    Anyway, thanks for all this – this is very interesting. I just wish the Platform SDK people would pick up on making sure all the RichEdit 4.1 functionality is documented, and maybe one day have the Windows team provide the latest RichEdit in Windows – for those of us writing Windows programs its depressing to see Apple, Linux et. al. getting updated UI components, while on Windows things like RichEdit aren’t updated for years … Anyway, I appreciate that that’s not your call!

  2. David Kinder says:

    After a bit of experimentation, it turns out that the message is WM_USER+232. The following code snippet works for me, inserting a 3×2 table. Neat :)

    TABLEROWPARMS rows;

    ZeroMemory(&rows,sizeof rows);

    rows.cbRow = sizeof (TABLEROWPARMS);

    rows.cbCell = sizeof (TABLECELLPARMS);

    rows.cCell = 3;

    rows.cRow = 2;

    rows.dxCellMargin = 50;

    rows.nAlignment = 1;

    rows.dyHeight = 400;

    rows.fIdentCells = 1;

    TABLECELLPARMS cells;

    ZeroMemory(&cells,sizeof cells);

    cells.dxWidth = 1000;

    cells.dxBrdrLeft = 1;

    cells.dyBrdrTop = 1;

    cells.dxBrdrRight = 1;

    cells.dyBrdrBottom = 1;

    cells.crBackPat = RGB(255,255,255);

    cells.crForePat = RGB(0,0,0);

    SendMessage(richHwnd,WM_USER+232,(WPARAM)&rows,(LPARAM)&cells);

  3. Alex says:

    Please, could you tell when more info about RichEdit weill be posted? I mean messages’ numeric values, table definitions and etc.

  4. Carc says:

    Yes! I agree with Alex!

    I mean messages values (WM_INSERTTABLEROW)!

  5. Tom Sherlock says:

    I was playing around with David’s code above and found that if you set rows.fIdentCells = 0 and then pass in a pointer to an array of N TABLECELLPARMS structures where N is the number of columns in the table, you can set the style of individual columns with a single EM_INSERTTABLEROW message.   I’m not sure how to set up individual rows to be different, or to have different types of cells.  Murray, are there any style setting messages for tables?

  6. Tom Sherlock says:

    Actually, I did find out how to make rows different — insert a table and then insert another table with a new style, or geometry, right below the first one.  With the second table, set the fMergeTop and fMergePrev members to 1, and the new table will attach itself to the first table — pretty cool!

  7. David Kinder says:

    Tom: Ah, I hadn’t figured out how to get that working. Excellent!

  8. What paragraphs are and how they are formatted are questions that continually come up both inside and

  9. Martin Osieka says:

    One observation using msftedit 5.41.15.1515 (XP SP3): Test states "Specifically it deletes the text (if any) currently selected by the selection" but EM_INSERTTABLEROW throws an error (0x80004005) if you try to replace a selection. Is it only possible to use EM_INSERTTABLEROW on a degenerated selection.

    It is nice to insert tables in this way, but as long as there are no messages to manipulate rows and cells attributes later on. So if there are notifications or messages please tell.

  10. Carc says:

    My alternate code for row insertion (C++). Run successfully

    bool RTFTableSupport::InsertRow(const HWND hwndRE)

    {

    ASSERT(IsWindow(hwndRE));

    CHARRANGE cr;

    SendMessage(hwndRE,EM_EXGETSEL,0,(LPARAM)&cr);

    CComPtr<ITextRange> Range=CTomUtil::GetITextRange(hwndRE,cr);

    ASSERT(Range);

    if (!Range)

    return false;

    Range->SetStart(cr.cpMin);

    const HRESULT hr=Range->EndOf(tomRow,tomMove,NULL);

    if (S_OK==hr) {

    Range->GetEnd(&cr.cpMax);

    cr.cpMax-=2;

    cr.cpMin=cr.cpMax;

    SendMessage(hwndRE,EM_EXSETSEL,0,(LPARAM)&cr);

    SendMessage(hwndRE,WM_KEYDOWN,VK_RETURN,0);

    SendMessage(hwndRE,WM_KEYUP,VK_RETURN,0);

    }

    return (S_OK == hr);

    }

  11. stev says:

    Hi  Carc,

    can you please specify your use of the TOM ?

    I didn’t find anything on the net of your usage

    CTomUtil::GetITextRange()

    I’m very much interested in table insertions in richedit controls.

    Thanks,

    Stev

  12. MurrayS says:

    You all are solving varous problems quite ably. The table facility in RE 41 is fairly limited with regard to resizing, but you can insert rows by inserting CR (U+000D) when the insertion point is displayed at the end of the table (actually the insertion point is in between the final CELL mark and the table row end delimiter <U+FFFB U+000D>. The table rows do resize automatically in the vertical direction.

    You can get an ITextRange from an ITextDocument as is documented in the SDK. But ITextRange has very limited support for tables.

    I’m hoping we can satisfy your requests for better RichEdit documentation and functionality in Windows. But it takes time, partly because of the extensive testing required by security concerns.

  13. Carc says:

    Hi, Stev.

    Sorry for the my short code sample.

    I use TOM (Text Object Model) very ofter. And I develome helper module for getting interface of TOM.

    This may be write e.g

    //getting ITextDocument interface from RichEdit (hwnd of RichEdit)

    ITextDocument* PlugTomUtil::GetDocument(const HWND hwnd)

    {

    IUnknown* pOle=NULL;

    if (!SendMessage(hwnd,EM_GETOLEINTERFACE,0,(LPARAM)&pOle))

    return NULL;

    ITextDocument* pDoc=NULL;

    const HRESULT hr=pOle->QueryInterface(IID_ITextDocument,(void**)&pDoc);

    pOle->Release();

    return (S_OK == hr ? pDoc : NULL);

    }

    and later

    ITextDocument* pDoc=PlugTomUtil::GetDocument(hwndRichEdit);

    ITextRange* pRange=NULL;

    pDoc->Range(begin_of_range_position,end_of_range_position,&pRange);

  14. Carc says:

    2Murray

    1. I use TOM for insertion CR symbols only at code way.

    2. How to move table columns size (or rows) at mouse? (RichEdit 5.0, XP SP3). How to implement the feature?

  15. MurrayS says:

    RichEdit 6.0 lets one resize table columns and rows by dragging column and row borders with the mouse, as in Word. This functionality isn’t exposed programmatically yet, but the methods to handle it are part of TOM2. It isn’t worth defining the methods, since they haven’t been implemented to date.

  16. Carc says:

    Thank You!

    Where get RichEdit 6.0 DLL? The DLL available in Windows XP?

  17. MurrayS says:

    The earlier post "Some RichEdit History" http://blogs.msdn.com/murrays/archive/2006/10/20/some-richedit-history.aspx describes how RichEdit 6.0 has been shipped. Basically it’s part of Office 2007.

  18. Carc says:

    I can use RichEdit 6.0 from VC code?

    My application use detection algorithm for RichEdit version "on fly". If available – use "RICHEDIT50W", other "RichEdit20W", other "RICHEDIT" class name.

    How use window class name for RichEdit 6.0?

  19. Carc says:

    How to disable Clear Type font method for RichEdit fonts?

  20. Carc says:

    How do I specify a transparent color of the table cells?