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
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;
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With