Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Tables in RichEdit in Delphi

I am attempting to use Tables in a TRichEdit control in Delphi XE2 Starter Edition. (In other words, I don’t have the source code for XE2 – but I do have it for TurboDelphi). I understand that the default RichEdit control does not use a version of MS RichEdit that supports tables, so I have subclassed it to use MS RichEdit v4.1 as described here 1 and here 6, and also modeled off the code in the JEDI TjvRichEdit. (for the sake of brevity, I have not included the code segment that determines RichEdit version numbers for DLL's other than v4.1, that I borrowed from JEDI. A simplified version is shown here.)

An MSDN blog 2 states that Windows messages to support RTF tables were an undocumented feature of MS RichEdit version 4.1, and that the EM_INSERTTABLE message has been available since Windows XP SP2. For more information on which versions were available when, see here 3.

A comment following this blog 2, posted by David Kinder on 26 Sep 2008, states he was able to get the EM_INSERTTABLE message to work with RichEdit v4.1, using the same code which I have shown below (except he wasn’t using Delphi).

For details on the EM_INSERTTABLE message and the structures that support it, see the MSDN docs 4 (which state they were introduced with Windows 8, but which clearly pre-dated that by at least two major OS versions). Also note that the definition(s) of the structures have changed somewhat since Murray wrote his blog 2 in 2008. I have searched to the ends of the internet and cannot find a MS richedit.h version that goes with RichEdit 4.1 and includes the “undocumented” structures TABLEROWPARMS and TABLECELLPARMS as they existed at that time, so I am limited to the MSDN docs as the exist for Win8 4 and Murray’s blog 2 as they allegedly existed in Win XP and Win7.

Here is my custom RichEdit:

unit MyRichEdit;
//Customized RichEdit to use MS RichEdit v4.1
// Some stuff borrowed from Michael Lam's REdit (ca. 1998), found on the Torry page.

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  StdCtrls, ComCtrls, Printers, RichEdit;

type

  {TableRowParms}
  PTableRowParms = ^TTableRowParms;

  _tableRowParms = packed record
     cbRow         : BYTE ;     // Count of bytes in this structure
     cbCell        : BYTE ;     // Count of bytes in TABLECELLPARMS
     cCell         : BYTE ;     // Count of cells
     cRow          : BYTE ;     // Count of rows
     dxCellMargin  : LONG ;     // Cell left/right margin (\trgaph)
     dxIndent      : LONG ;     // Row left (right if fRTL indent (similar to \trleft)
     dyHeight      : LONG ;     // Row height (\trrh)
     nAlignment{:3}: DWORD;    // Row alignment (like PARAFORMAT::bAlignment, \trql, trqr, \trqc)
     fRTL{:1}      : DWORD;    // Display cells in RTL order (\rtlrow)
     fKeep{:1}     : DWORD;    // Keep row together (\trkeep}
     fKeepFollow{:1} : DWORD;    // Keep row on same page as following row (\trkeepfollow)
     fWrap{:1}       : DWORD;    // Wrap text to right/left (depending on bAlignment) (see \tdfrmtxtLeftN, \tdfrmtxtRightN)
     fIdentCells{:1} : DWORD;   // lparam points at single struct valid for all cells
     //cpStartRow    : LONG ;   // not in Murray's blog version, so commented here...
     //bTableLevel   : BYTE;    // not in Murray's blog version
     //iCell         : BYTE;    // not in Murray's blog version
  end;

  TABLEROWPARMS = _tableRowParms;
  TTableRowParms = TABLEROWPARMS;


  {TableCellParms}
  PTableCellParms = ^TTableCellParms;

  _tableCellParms = packed record
     dxWidth        : LONG    ;      // Cell width (\cellx)
     nVertAlign{:2}   : WORD    ;      // Vertical alignment (0/1/2 = top/center/bottom  \clvertalt (def), \clvertalc, \clvertalb)
     fMergeTop{:1}    : WORD    ;      // Top cell for vertical merge (\clvmgf)
     fMergePrev{:1}   : WORD    ;      // Merge with cell above (\clvmrg)
     fVertical{:1}    : WORD    ;      // Display text top to bottom, right to left (\cltxtbrlv)
     wShading       : WORD    ;      // Shading in .01% (\clshdng) e.g., 10000 flips fore/back
     dxBrdrLeft     : SHORT   ;      // Left border width (\clbrdrl\brdrwN) (in twips)
     dyBrdrTop      : SHORT   ;      // Top border width (\clbrdrt\brdrwN)
     dxBrdrRight    : SHORT   ;      // Right border width (\clbrdrr\brdrwN)
     dyBrdrBottom   : SHORT   ;      // Bottom border width (\clbrdrb\brdrwN)
     crBrdrLeft     : COLORREF;      // Left border color (\clbrdrl\brdrcf)
     crBrdrTop      : COLORREF;      // Top border color (\clbrdrt\brdrcf)
     crBrdrRight    : COLORREF;      // Right border color (\clbrdrr\brdrcf)
     crBrdrBottom   : COLORREF;      // Bottom border color (\clbrdrb\brdrcf)
     crBackPat      : COLORREF;      // Background color (\clcbpat)
     crForePat      : COLORREF;      // Foreground color (\clcfpat)
  end;

  TABLECELLPARMS = _tableCellParms;
  TTableCellParms = TABLECELLPARMS;

  TMyRichEdit = class(ComCtrls.TRichEdit)
  private
    function GetRTF: string;              // get the RTF string
    procedure SetRTF(InRTF: string);      // set the RTF string
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  published
    property RTFText: string read GetRTF write SetRTF;
  end;

//--------------------------------------------------------------
// GLOBAL VARIABLES
//--------------------------------------------------------------
var
  RichEditVersion    : Integer;         //Version of the MS Windows RichEdit DLL

const
  RichEdit10ModuleName = 'RICHED32.DLL';
  RichEdit20ModuleName = 'RICHED20.DLL';
  RichEdit41ModuleName = 'MSFTEDIT.DLL';
  MSFTEDIT_CLASS = 'RichEdit50W'; //goes with RichEdit 4.1 (beginning with Win XP SP2)

  EM_INSERTTABLE = WM_USER + 232;


implementation


function TMyRichEdit.GetRTF: string;
var FStream : TStringStream;
begin
    // get the RTF string
    FStream := TStringStream.Create;    // RTF stream
    FStream.Clear;
    FStream.Position := 0;
    Lines.SaveToStream(FStream);
    Result := FStream.DataString;
    FStream.Free;                   // free the RTF stream
end; //ok

procedure TMyRichEdit.SetRTF(InRTF: string);
var FStream : TStringStream;
begin
    // set the RTF string
    // LoadFromStream uses an EM_STREAMIN windows msg, which by default REPLACES the contents of a RichEdit.
    FStream := TStringStream.Create;    // RTF stream
    FStream.Clear;
    FStream.Position := 0;
    FStream.WriteString(InRTF);
    FStream.Position := 0;
    Lines.LoadFromStream(FStream);
    Self.Modified := false;
    FStream.Free;                   // free the RTF stream
end; //ok


//===========================================================================
//Defaults: RICHEDIT_CLASS = 'RichEdit20W'; RICHEDIT_CLASS10A = 'RICHEDIT';
//It needs to use RichEdit50W for version 4.1, which I defined in a constant above as MSFTEDIT_CLASS.

procedure TMyRichEdit.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);

  If RichEditVersion = 1 then CreateSubClass(Params, RICHEDIT_CLASS10A)
  else If RichEditVersion = 4 then CreateSubClass(Params, MSFTEDIT_CLASS)
  else CreateSubClass(Params, RICHEDIT_CLASS);
end;


//================================================================
{Initialization Stuff}
//================================================================
var
  GLibHandle: THandle = 0;

procedure InitRichEditDll;
begin
  //Try to load MS RichEdit v 4.1 into memory...
  RichEditVersion := 4;
  GLibHandle := SafeLoadLibrary(RichEdit41ModuleName);
  if (GLibHandle > 0) and (GLibHandle < HINSTANCE_ERROR) then
     GLibHandle := 0; //this means it could not find the DLL or it didn't load right.
  if GLibHandle = 0 then begin
     RichEditVersion := 2;
     GLibHandle := SafeLoadLibrary(RichEdit20ModuleName);
     if (GLibHandle > 0) and (GLibHandle < HINSTANCE_ERROR) then
        GLibHandle := 0;
     if GLibHandle = 0 then begin
        RichEditVersion := 1;
        GLibHandle := SafeLoadLibrary(RichEdit10ModuleName);
        if (GLibHandle > 0) and (GLibHandle < HINSTANCE_ERROR) then begin
           RichEditVersion := 0;
           GLibHandle := 0;
        end;
     end;
  end;
end;

procedure FinalRichEditDll;
begin
  if GLibHandle > 0 then
  begin
    FreeLibrary(GLibHandle);
    GLibHandle := 0;
  end;
end;


initialization
  InitRichEditDll;

finalization
  FinalRichEditDll;


End.

Usage:

Uses … MyRichEdit …

type

  TRichEdit = class(TMyRichEdit);

  TfrmEdit = class(TForm)
…
    memNotes: TRichEdit;
…

end;


procedure TfrmEdit.actTableAddExecute(Sender: TObject);
var
   rows: TABLEROWPARMS;
   cells: TABLECELLPARMS;
   rc : LRESULT; 
begin
   //Insert a table into the RTF.
   ZeroMemory(@rows,sizeof(rows));
   rows.cbRow := sizeof(TABLEROWPARMS);
   rows.cbCell := sizeof(TABLECELLPARMS);
   rows.cCell := 3;
   rows.cRow := 2;
   rows.dxCellMargin := 5; //50
   rows.nAlignment := 1;
   rows.dyHeight := 100;  //400
   rows.fIdentCells := 1;
   rows.fRTL := 0;
   rows.fKeep := 1;
   rows.fKeepFollow := 1;
   rows.fWrap := 1;
   //rows.cpStartRow := -1;

   ZeroMemory(@cells,sizeof(cells));
   cells.dxWidth := 600; //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);
   cells.nVertAlign := 0;
   //cells.fMergeTop := 1;
   //cells.fMergePrev := 1;
   cells.fVertical := 1;

   rc := SendMessage(memNotes.Handle,EM_INSERTTABLE, WPARAM(@rows),LPARAM(@cells));
   //rc := memNotes.Perform(EM_INSERTTABLE, WPARAM(@rows),LPARAM(@cells));

end;

Upon execution, rc contains -2147024809 (E_INVALIDARG). I don’t have a good understanding of why this fails, or what the problem is with the message arguments. As a disclaimer, I am new to working with RichEdit in Delphi, but I tried to learn as much as possible before posting for help.

In my extensive searching, I found this website 5 which may help narrow the problem. This site hosts a utility called RTFLabel. Download the zip file and have a look at richedit2.pas, where they explain that “The definition of CHARFORMAT2A and CHARFORMAT2W in richedit.h (2005 SDK) has an error in the C part”, and that they needed to insert a new dummy field to “fix” the byte alignment of the structure in order to make it work correctly with Delphi. I have a feeling there may be similar alignment issues with one or both of the TABLEROWPARMS and TABLECELLPARMS structures which is causing this error.

I would like some help to figure out why the SendMessage is returning a E_INVALIDARG, and what I can do to fix it. Any assistance would be greatly appreciated!
-Jeff Aylor

Referenced websites:
1 [http://fgaillard.com/2010/09/using-richedit-4-1-with-d2010/]1
2 [http://blogs.msdn.com/b/murrays/archive/2008/09/15/richedit-s-nested-table-facility.aspx]2
3 [http://blogs.msdn.com/b/murrays/archive/2006/10/14/richedit-versions.aspx]3
4 [http://msdn.microsoft.com/en-us/library/windows/desktop/hh768373%28v=vs.85%29.aspx]4
5 [http://flocke.vssd.de/prog/code/pascal/rtflabel/]5
6 [Delphi 7 TRichTextEdit Text in a box not displaying correctly6

like image 291
Jeff Avatar asked Mar 27 '14 13:03

Jeff


2 Answers

here are the right structures...

  _tableRowParms = record
     cbRow           : BYTE;        // Count of bytes in this structure
     cbCell          : BYTE;        // Count of bytes in TABLECELLPARMS
     cCell           : BYTE;        // Count of cells
     cRow            : BYTE;        // Count of rows
     dxCellMargin    : LONGINT;     // Cell left/right margin (\trgaph)
     dxIndent        : LONGINT;     // Row left (right if fRTL indent (similar to \trleft)
     dyHeight        : LONGINT;     // Row height (\trrh)
     nParams         : DWORD;       // 0 - 2 bits - Row alignment (like PARAFORMAT::bAlignment, 1/2/3) (\trql, trqr, \trqc)
                                    // 3 bit - Display cells in RTL order (\rtlrow)
                                    // 4 bit - Keep row together (\trkeep}
                                    // 5 bit - Keep row on same page as following row (\trkeepfollow)
                                    // 6 bit - Wrap text to right/left (depending on bAlignment) (see \tdfrmtxtLeftN, \tdfrmtxtRightN)
                                    // 7 bit - lparam points at single struct valid for all cells
     cpStartRow      : LONGINT;     // The character position that indicates where to insert table. A value of –1 indicates the character position of the selection. 
     bTableLevel     : BYTE;        // The table nesting level (EM_GETTABLEPARMS only).
     iCell           : BYTE;        // The index of the cell to insert or delete (EM_SETTABLEPARMS only).
  end;
  TABLEROWPARMS  = _tableRowParms;
  TTableRowParms = TABLEROWPARMS;
  PTableRowParms = ^TTableRowParms;


  _tableCellParms = record
     dxWidth         : LONGINT;     // Cell width (\cellx)
     nParams         : Word;        // 0 - 1 bits - Vertical alignment (0/1/2 = top/center/bottom) (\clvertalt (def), \clvertalc, \clvertalb)
                                    // 2 bit - Top cell for vertical merge (\clvmgf)
                                    // 3 bit - Merge with cell above (\clvmrg)
                                    // 4 bit - Display text top to bottom, right to left (\cltxtbrlv)
                                    // 5 bit - Start set of horizontally merged cells (\clmgf).
                                    // 6 bit - Merge with the previous cell (\clmrg).      
     wShading       : WORD;         // Shading in .01% (\clshdng) e.g., 10000 flips fore/back
     dxBrdrLeft     : SHORT;        // Left border width (\clbrdrl\brdrwN) (in twips)
     dyBrdrTop      : SHORT;        // Top border width (\clbrdrt\brdrwN)
     dxBrdrRight    : SHORT;        // Right border width (\clbrdrr\brdrwN)
     dyBrdrBottom   : SHORT;        // Bottom border width (\clbrdrb\brdrwN)
     crBrdrLeft     : COLORREF;     // Left border color (\clbrdrl\brdrcf)
     crBrdrTop      : COLORREF;     // Top border color (\clbrdrt\brdrcf)
     crBrdrRight    : COLORREF;     // Right border color (\clbrdrr\brdrcf)
     crBrdrBottom   : COLORREF;     // Bottom border color (\clbrdrb\brdrcf)
     crBackPat      : COLORREF;     // Background color (\clcbpat)
     crForePat      : COLORREF;     // Foreground color (\clcfpat)
  end;
  TABLECELLPARMS  = _tableCellParms;
  TTableCellParms = TABLECELLPARMS;
  PTableCellParms = ^TTableCellParms;
like image 180
Erwin Sienknecht Avatar answered Oct 23 '22 10:10

Erwin Sienknecht


tl;dr:

Don't use it for anything other than it is advertised/documented for (i.e lower than W8), even then I would keep an open eye. It's a mess.


Notice that there are other differences between the blog post you take your records from and the documentation, other than the additional three fields of TABLEROWPARMS. This is in TABLECELLPARMS. Here's a side by side comparison, on the left is the record from the blog post, on the right is the record from the documentation.

typedef struct _tableCellParms {           typedef struct _tableCellParms {
    LONG    dxWidth;                         LONG     dxWidth;
    WORD    nVertAlign:2;                    WORD     nVertAlign:2;
    WORD    fMergeTop:1;                     WORD     fMergeTop:1;
    WORD    fMergePrev:1;                    WORD     fMergePrev:1;
    WORD    fVertical:1;                     WORD     fVertical:1;
                                             WORD     fMergeStart:1;
                                             WORD     fMergeCont:1;
    WORD    wShading;                        WORD     wShading;
    SHORT   dxBrdrLeft;                      SHORT    dxBrdrLeft;
    SHORT   dyBrdrTop;                       SHORT    dyBrdrTop;
    SHORT   dxBrdrRight;                     SHORT    dxBrdrRight;
    SHORT   dyBrdrBottom;                    SHORT    dyBrdrBottom;
    COLORREF crBrdrLeft;                     COLORREF crBrdrLeft;
    COLORREF crBrdrTop;                      COLORREF crBrdrTop;
    COLORREF crBrdrRight;                    COLORREF crBrdrRight;
    COLORREF crBrdrBottom;                   COLORREF crBrdrBottom;
    COLORREF crBackPat;                      COLORREF crBackPat;
    COLORREF crForePat;                      COLORREF crForePat;
} TABLECELLPARMS;                          } TABLECELLPARMS;


Here you see two additional fields inserted in the middle of the record intuitively. Or, one of them is wrong, perhaps both... But we know from the blog post that there are people who have been able to make it work with the one on the left. Maybe there's a specific version against a specific dll.

Anyway, considering there's a very good probability that the cause of the E_INVALIDARG is the size of the structs, since even with a minimal test it fails to work, this led me to test using brute force to determine correct record sizes.

var
   rows: TABLEROWPARMS;
   cells: TABLECELLPARMS;
begin
   ZeroMemory(@rows,sizeof(rows));
   rows.cbRow := 1;
   rows.cbCell := 1;
   rows.cCell := 1;
   rows.cRow := 1;

   ZeroMemory(@cells,sizeof(cells));

   while SendMessage(RichEdit1.Handle, EM_INSERTTABLE, 
                     WPARAM(@rows), LPARAM(@cells)) <> S_OK do begin
     if rows.cbCell < 120 then  // arbitrary upper limit 
       Inc(rows.cbCell)
     else begin
       Inc(rows.cbRow);
       rows.cbCell := 1;
     end;
     if rows.cbRow = 120 then
      raise Exception.Create('no match');
   end;
end;

Putting a breakpoint to the end of the procedure, rows.cbRow comes up '28' and rows.cbCell comes up '40'. They are pretty smaller than both references. I also tested for a possible match larger than the first hit, there are none. My test is against 'msftedit.dll' version 5.41.21.2510, residing in '\syswow64' of a W7 box, using XE2.

So where should we cut the structs, from the end? As we see from the above references, probably not. I don't see any sound way to advance from here, but I'm posting my best try in case you have a similar environment and want to carry on (which I do not recommend - note I had to change the order of fields to come this far). Which is definitely incorrect as it's not able to insert a table having columns more than one for instance.

  _tableRowParms = packed record
     cbRow         : BYTE ;
     cbCell        : BYTE ;
     cCell         : BYTE ;
     cRow          : BYTE ;
     dxCellMarginOrVertAlign  : LONG ;  // when there's more than one cell
     dxIndent      : LONG ;
     dyHeight      : LONG ;
     nAlignment    : DWORD;
     fRTL          : DWORD;
     fKeep         : DWORD;
  end;

  _tableCellParms = packed record
     dxWidth        : LONG    ;
     nVertAlign     : WORD    ;
     fVertical      : WORD    ;
     dxBrdrLeft     : SHORT   ;
     dyBrdrTop      : SHORT   ;
     dxBrdrRight    : SHORT   ;
     dyBrdrBottom   : SHORT   ;
     crBrdrLeft     : COLORREF;
     crBrdrTop      : COLORREF;
     crBrdrRight    : COLORREF;
     crBrdrBottom   : COLORREF;
     crBackPat      : COLORREF;
     crForePat      : COLORREF;
  end;

..

var
   rows: TABLEROWPARMS;
   cells: TABLECELLPARMS;
   rc : LRESULT;
begin
   ZeroMemory(@rows,sizeof(rows));

   rows.cbRow := sizeof(TABLEROWPARMS);
   rows.cbCell := sizeof(TABLECELLPARMS);
   rows.cCell := 1;                // ??
   rows.cRow := 3;
   rows.dxCellMarginOrVertAlign := 120;
   rows.dxIndent := 200;
   rows.dyHeight := 400;
   rows.nAlignment := 1;                    // don't leave at 0
   rows.fRTL := 0;                          // ???
   rows.fKeep := 0;                         // ???

   ZeroMemory(@cells,sizeof(cells));

   cells.dxWidth := 1000;
   cells.nVertAlign := 1;
   cells.fVertical := 1;
   cells.dxBrdrLeft := 50;
   cells.dyBrdrTop := 10;
   cells.dxBrdrRight := 50;
   cells.dyBrdrBottom := 20;
   cells.crBrdrLeft := RGB(255,0,0);
   cells.crBrdrTop := RGB(0, 255, 0);
   cells.crBrdrRight := RGB(0, 0, 255);
   cells.crBrdrBottom := RGB(255, 255, 0);
   cells.crBackPat := RGB(255, 255, 255);
   cells.crForePat := RGB(128, 64, 64);     // ?

   rc := SendMessage(RichEdit1.Handle,EM_INSERTTABLE, WPARAM(@rows),LPARAM(@cells));
end;
like image 1
Sertac Akyuz Avatar answered Oct 23 '22 12:10

Sertac Akyuz