TRichEdit behaves differently under Unicode.

EmboldenDollarAmounts

The problem.

One of my customers contacted me recently to explain that code which functioned under Delphi 2005 was failing under Delphi XE8. Essentially, this code was intended to alter the contents of a TRichEdit in order to embolden currency amounts denoted by a ‘$’ character.

The existing code uses the ‘SelStart’ and ‘SelLength’ properties of the TRichEdit class to select a piece of text which needs to be emboldened. The selection’s text properties are then altered.

Unfortunately the piece of text which was emboldened was not correctly selected, it would be several characters off the correct position. This had been working code under D2005, so what went wrong?

The Cause.

I suspect the cause of this problem is the underlying windows API calls, which have been moved from the ANSI versions to the Unicode or WideChar versions. Under Unicode the carriage return character has no meaning, it is essentially ignored, and it appears that this character is being ignored when the ‘SelLength’ property is evaluated. For this reason, any time you make a selection within a TRichEdit control using the ‘SelStart’ and ‘SelLength’ properties, you must first manually count the carriage return characters (#13) and subtract them.

Solution One…

As you should guess from the heading, this is not my final solution, however, it demonstrates how the problem can be avoided by recoding. The following code snippet is an example of searching a TRichEdit for ‘$’ and then emboldening numerical amounts which follow this character (including the ‘$’ it’s self…)

procedure EmboldenDollarAmounts( aRichEdit: TRichEdit );
var
  WorkStr: string;
  LineCounter: int32;
  chidx: int32;
begin
  WorkStr := aRichEdit.Lines.Text;
  LineCounter := 1;
  chidx := 1;
  repeat
    //- Count lines passed in parsing
    if WorkStr[chidx] = chr(13) then begin
      inc(LineCounter);
    end;
    //- Check for '$'
    if WorkStr[chidx]='$' then begin
      aRichEdit.SelStart := chidx - (LineCounter);
      while CharInSet(WorkStr[chidx], ['$','0'..'9','.']) do inc(chidx);
      aRichEdit.SelLength := (chidx-aRichEdit.SelStart)-LineCounter;
      aRichEdit.SelAttributes.Style := [fsBold];
    end else begin
      inc(chidx);
    end;
  until chidx>=Length(WorkStr);
end;

This solution is not very robust, and means altering your code anywhere that you’ve used ‘SelStart’ or ‘SelLength’ but perhaps there is a better way.

Solution Two.

Rather than altering your code everywhere that you’ve used SelStart and SelLength. You could instead create your own subclass of TRichEdit and override the SetSelStart() and SetSelLength() methods to automatically adjust the property values.

I gave this solution a try, and came up with the following class…

unit crRichEdit;

interface

uses
  System.SysUtils, System.Classes, Vcl.Controls, Vcl.StdCtrls, Vcl.ComCtrls;

type
  TCRRichEdit = class(TRichEdit)
  private
  protected
    // Override setters for SelStart and SelLength.
    procedure SetSelLength(Value: Integer); override;
    procedure SetSelStart(Value: Integer); override;
  public
    constructor Create( aOwner: TComponent ); override;
  published
    //- Surface inherited properties.
    property Align;
    property Alignment;
    property Anchors;
    property BevelEdges;
    property BevelInner;
    property BevelOuter;
    property BevelKind default bkNone;
    property BevelWidth;
    property BiDiMode;
    property BorderStyle;
    property BorderWidth;
    property Color;
    property Ctl3D;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property HideSelection;
    property HideScrollBars;
    property ImeMode;
    property ImeName;
    property Constraints;
    property Lines;
    property MaxLength;
    property ParentBiDiMode;
    property ParentColor;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PlainText;
    property PopupMenu;
    property ReadOnly;
    property ScrollBars;
    property ShowHint;
    property TabOrder;
    property TabStop default True;
    property Touch;
    property Visible;
    property WantTabs;
    property WantReturns;
    property WordWrap;
    property StyleElements;
    property Zoom;
    property OnChange;
    property OnClick;
    property OnContextPopup;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnGesture;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnMouseActivate;
    property OnMouseDown;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelDown;
    property OnMouseWheelUp;
    property OnProtectChange;
    property OnResizeRequest;
    property OnSaveClipboard;
    property OnSelectionChange;
    property OnStartDock;
    property OnStartDrag;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TCRRichEdit]);
end;

{ TCRRichEdit }

constructor TCRRichEdit.Create(aOwner: TComponent);
begin
  inherited Create( aOwner );
  if (aOwner is TWinControl) then begin
    Parent := (aOwner as TWinControl);
  end;
end;

procedure TCRRichEdit.SetSelLength(Value: Integer);
var
  WorkStr: string;
  idx: int32;
  CRCount: int32;
begin
  if Value<>SelLength then begin
    if (Value>0) and
       (((inherited GetSelStart)+Value)<=Length(Self.Lines.Text)) then begin
      // Count the carriage returns..
      CRCount := 0;
      WorkStr := Self.Lines.Text;
      for idx := 1 to ((inherited GetSelStart)+Value) do begin
        if WorkStr[idx]=chr(13) then begin
          inc(CRCount);
        end;
      end;
      // Set the actual SelStart
      inherited SetSelLength( Value - CRCount );
    end;
  end;
end;

procedure TCRRichEdit.SetSelStart(Value: Integer);
var
  WorkStr: string;
  idx: int32;
  CRCount: int32;
begin
  if Value<>SelStart then begin
    if (Value>0) and (Value<=Length(Self.Lines.Text)) then begin
      // Count the carriage returns..
      CRCount := 0;
      WorkStr := Self.Lines.Text;
      for idx := 1 to Value do begin
        if WorkStr[idx]=chr(13) then begin
          inc(CRCount);
        end;
      end;
      // Set the actual SelStart
      inherited SetSelStart( Value - CRCount );
    end;
  end;
end;

end.

Using the above class, you should not need to alter your original code. So I restored my example code to what I’d expect if I’d written it for a pre-unicode version of Delphi…

procedure EmboldenDollarAmounts( aRichEdit: TRichEdit );
var
  WorkStr: string;
  chidx: int32;
begin
  WorkStr := aRichEdit.Lines.Text;
  chidx := 1;
  repeat
    //- Check for '$'
    if WorkStr[chidx]='$' then begin
      aRichEdit.SelStart := pred(chidx);
      while CharInSet(WorkStr[chidx], ['$','0'..'9','.',',']) do inc(chidx);
      aRichEdit.SelLength := pred((chidx-aRichEdit.SelStart));
      aRichEdit.SelAttributes.Style := [fsBold];
    end else begin
      inc(chidx);
    end;
  until chidx>=Length(WorkStr);
end;

Then I replaced the TRichEdit on my form with a TCRRichEdit (my new control).  The result…

EmboldenDollarAmounts

Conclusion

Solution two would appear to be the easiest transition from pre-unicode code. You could use the IDE or your favorite text editor to scan both your .pas and .dfm files and replace TRichEdit with TCRRichEdit. In theory, the code would then build and work as it should without alteration.

For your convenience, I’ve uploaded my TCRRichEdit component source, a package to install it, and a test application to my public source code repository…

*WARNING* Do with this code as you see fit, however, be aware that this code is untested and should not be considered suitable for insertion into your applications.

Edit: The subversion server has been decommissioned. Replaced svn link with downloadable zip file, download here: CRRichEdit

Thanks for reading!

Facebooktwittergoogle_plusredditpinterestlinkedintumblrmail

Leave a Reply