在Delphi中使用RichEdit表格

9

我正在尝试在Delphi XE2 Starter Edition的TRichEdit控件中使用表格。(换句话说,我没有XE2的源代码,但我有TurboDelphi的源代码)。我了解默认的RichEdit控件不使用支持表格的MS RichEdit版本,因此我对其进行了子类化,以使用MS RichEdit v4.1,如此处1和此处6所述,并且还参考了JEDI TjvRichEdit中的代码。(为了简洁起见,我没有包含决定除v4.1之外的DLL的RichEdit版本号的代码段,这是我从JEDI借鉴的。这里显示了一个简化版本。)
一篇 MSDN 博客2指出,支持 RTF 表格的 Windows 消息是 MS RichEdit 4.1 的一个未记录特性,并且自 Windows XP SP2 起,EM_INSERTTABLE 消息已经可用。有关每个版本的可用性的更多信息,请参见此处 3

在这篇博客2之后的一条评论,由David Kinder于2008年9月26日发布,他表示他能够使用我下面展示的相同代码(除了他没有使用Delphi)来使EM_INSERTTABLE消息在RichEdit v4.1中运行。

有关EM_INSERTTABLE消息及其支持的结构的详细信息,请参阅MSDN文档4(该文档称它们是在Windows 8中引入的,但至少比那之前的两个主要操作系统版本早)。还要注意,自Murray在2008年撰写他的博客2以来,这些结构的定义已经有所改变。我已经搜索了整个互联网,找不到与RichEdit 4.1配套的MS richedit.h版本,其中包括当时存在的“未公开”结构TABLEROWPARMS和TABLECELLPARMS,因此我只能使用MSDN文档作为Win8 4和据称存在于Win XP和Win7的Murray博客2

这是我的自定义 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.

使用方法:

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;

在执行时,rc包含-2147024809(E_INVALIDARG)。我不太理解为什么会失败,或者消息参数存在什么问题。声明一下,我是新手,正在使用Delphi中的RichEdit,但在求助之前,我尽可能地学习了很多。
在我的广泛搜索中,我发现了这个网站5,它可能有助于缩小问题范围。该网站托管了一个名为RTFLabel的实用程序。下载zip文件并查看richedit2.pas,在那里他们解释说,“richedit.h(2005 SDK)中CHARFORMAT2A和CHARFORMAT2W的定义在C部分中存在错误”,并且他们需要插入一个新的虚拟字段以“修复”结构体的字节对齐,以便使其与Delphi正常工作。我感觉TABLEROWPARMS和TABLECELLPARMS结构中可能存在类似的对齐问题,导致此错误。

我希望能得到帮助,找出为什么SendMessage返回E_INVALIDARG错误,并了解我可以采取哪些措施来解决。非常感谢您的帮助!
-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

你使用 packed 关键字在这两个记录(结构体)定义中有特定的原因吗?我在 API 文档中没有看到它们应该被打包的任何迹象。 - Ken White
我从RTFLabel中RichEdit2.pas的CHARFORMAT2定义中获得了这些信息,网址在这里:http://flocke.vssd.de/prog/code/pascal/rtflabel。我将Packed去掉后仍然得到相同的错误。 - Jeff
2个回答

2

这里是正确的结构...

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

感谢您的发布!我可能要等一周左右才能检查它,但我会尽快尝试。 - Jeff

1

tl;dr:

不要将其用于除了广告/文档之外的任何其他用途(即低于W8),即使是这样,我也会保持警惕。这很混乱。


注意到除了TABLEROWPARMS的三个附加字段之外,博客文章和文档之间存在其他差异。这在TABLECELLPARMS中体现。下面是左右对比,左边是博客文章中的记录,右边是文档中的记录。
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;

这里您可以直观地看到在记录中间插入了两个额外的字段。或者,其中一个是错误的,也许两个都是...但我们从博客文章中知道有人已经能够使用左侧的那个字段使其正常工作。也许有一个特定版本针对特定的dll。无论如何,考虑到E_INVALIDARG的原因很可能是结构体的大小,因为即使进行最小的测试也无法正常工作,这促使我尝试使用蛮力来确定正确的记录大小。
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;

将断点放到程序末尾,rows.cbRow 的值为'28',rows.cbCell 的值为'40'。它们比两个参考值小得多。我还测试了可能大于第一个匹配项的其他内容,但没有。我的测试针对的是 'msftedit.dll' 版本 5.41.21.2510,在 W7 系统的 '\syswow64' 中,使用 XE2。

那么我们应该从哪里截取结构体呢?如上所述的参考值,很可能不是从结构体的末尾开始截取。我看不出有任何可靠的方法来继续下去,但我会发布我的最佳尝试,以防您有类似的环境并想要继续(我不建议这样做 - 请注意我必须更改字段的顺序才能达到这一步)。这肯定是不正确的,因为它无法插入具有多个列的表格。

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

感谢您的回复。我也注意到了TABLECELLPARMS中的差异,并尝试过带和不带这两个“额外”字段的两种方式。根据Ken上面的建议,我已经在记录定义中删除了“packed”,因为至少在TurboDelphi源代码中,所有的RichEdit记录都是没有它定义的。但仍然没有任何进展。 - Jeff
我注意到一件事,在这个网站https://www.devexpress.com/Support/Center/Question/Details/Q422389上 - 有一个关于EM_INSERTTABLE消息在名为cxRichEdit的组件上无法工作的参考。他们从未发布过后续,所以我不知道他们是否让它起作用了。cxRichEdit是否成为JEDI的一部分?还是另一个不同的组件? - Jeff
@Jeff - 运行我发布的暴力代码,这就是我发布它的原因,让你确定在你的环境中正确的结构大小。它输出了什么?2)cxRichEdit不是JEDI的一部分,据我所知,它吸收的唯一富文本编辑控件是RxRichEdit。1)当您不知道记录的确切定义及其对齐方式时,请使用packed,然后测试以找出需要使用的填充字节。然后,如果它应该具有另一种对齐方式,则可以将其转换为一些合理的对齐方式。 - Sertac Akyuz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接