Firemonkey Rotate Text

4.9k views Asked by At

I want to draw Text on a canvas. To do the rotation I used the following code from https://forums.embarcadero.com/thread.jspa?messageID=440010

//bm is a TImage    
a := 45;
c:= bm.Canvas;
CurrentM := c.Matrix;
a:=-radian(a);
m.m11:= cos(a); m.m12:=sin(a); m.m13:=0;
m.m21:=-sin(a); m.m22:=cos(a); m.m23:=0;
m.m31:=0;       m.m32:=0;      m.m33:=1;
c.setmatrix(M);

c.BeginScene;
    c.filltext(rectf(100,100,5000,5000), 'test rotated string', false,1,[],ttextalign.taLeading,ttextalign.taLeading);
c.EndScene;

This works fine. I have set my rectangle's right and bottom to 5000 so that I do not have to be worried about my rectangle being to small.

The problem is that I now want to change my TextAlignment properties. So to draw text from right to left I had to adjust my rectangle and then draw it in the following way:

c.BeginScene;
    c.filltext(rectf((100 - 5000),100,100,5000), 'test rotated string', false,1,[],ttextalign.taTrailing,ttextalign.taLeading);
c.EndScene;

So basically I moved the x value of my rectangle's TopLeft and moved it back 5000 (again I am using 5000 to make sure my text fit). I then set the x value of my rectangle's bottom right to where the x value was in my previous example's rectangle's TopLeft. This work fine for a 0 degree rotation, but as soon as I change the degrees I does not draw my text at the correct place. I assume this is because the text will rotate around the rectangle's TopLeft position (which is altered to make the text write from right to left).

3

There are 3 answers

1
Ingword On
 procedure TmsLineWithArrow.DoDrawTo(const aCanvas: TCanvas;
  const aOrigin: TPointF);
var
 l_Proxy : TmsShape;
 l_OriginalMatrix: TMatrix;
 l_Matrix: TMatrix;
 l_Angle : Single;
 l_CenterPoint : TPointF;

 l_TextRect : TRectF;
begin
 inherited;

 aCanvas.BeginScene;  

 if (StartPoint <> FinishPoint) then
 begin
  l_OriginalMatrix := aCanvas.Matrix;
  try
   l_Proxy := TmsSmallTriangle.Create(FinishPoint);
   try
    // in Radian
    l_Angle := GetArrowAngleRotation;

    // create a point around which will rotate
    l_CenterPoint := TPointF.Create(FinishPoint.X, FinishPoint.Y);

    l_Matrix := l_OriginalMatrix;
    l_Matrix := l_Matrix * TMatrix.CreateTranslation(-l_CenterPoint.X,-l_CenterPoint.Y);
    l_Matrix := l_Matrix * TMatrix.CreateRotation(l_Angle);
    l_Matrix := l_Matrix * TMatrix.CreateTranslation(l_CenterPoint.X,l_CenterPoint.Y);

    aCanvas.SetMatrix(l_Matrix);

    l_Proxy.DrawTo(aCanvas, aOrigin);
   finally
    FreeAndNil(l_Proxy);
   end;//try..finally
  finally
    aCanvas.SetMatrix(l_OriginalMatrix);
    aCanvas.EndScene;
  end;
 end;//(StartPoint <> FinishPoint)
end;

This code working in XE5 Firemonkey application. all source here https://bitbucket.org/ingword/mindstream

0
Mavati On

For people interested, here you will find the C++Builder version I use for FMX (tested on 10.2 / Tokyo) :

Function :

// Draw rotated text on pMainBitmap ; Rot = should be multiple of 90° !

void DrawRotatedText( TBitmap * pMainBitmap, TRectF TextDestRect, String StrTxt, int Rot )

(add of course "{" and "}" between the following code... always an error with the editor to post this answer...)

int SizeTextW = pMainBitmap->Canvas->TextWidth( StrTxt );
int SizeTextH = pMainBitmap->Canvas->TextHeight( StrTxt );
int SizeTextMax = (SizeTextW>SizeTextH)?SizeTextW:SizeTextH;
TRectF TheTextRect;
TheTextRect.init( 0, 0, SizeTextMax, SizeTextMax );

TBitmap * pBitmapText = new TBitmap( SizeTextMax, SizeTextMax );
if ( pBitmapText )
{
    /* background color used */
    pBitmapText->Clear( claBlack );

    pBitmapText->Canvas->BeginScene();
    // use same color than main bitmap for text
    pBitmapText->Canvas->Fill->Color = pMainBitmap->Canvas->Fill->Color;
    pBitmapText->Canvas->FillText(TheTextRect, StrTxt, false, 100,
        TFillTextFlags()/* << TFillTextFlag::RightToLeft*/, TTextAlign::Center,
        TTextAlign::Center);
    pBitmapText->Canvas->EndScene();
    // Canvas->EndScene must be done before doing bitmap rotate/flip...!
    if( Rot==180 )
        pBitmapText->FlipVertical( );
    else if ( Rot!=0 )
        pBitmapText->Rotate( Rot );

    int PosSrcX = 0;
    int PosSrcY = 0;
    if ( SizeTextW>SizeTextH )
        PosSrcX = (SizeTextMax-SizeTextH)/2;
    else
        PosSrcY = (SizeTextMax-SizeTextW)/2;
    TheTextRect.init( PosSrcX, PosSrcY, PosSrcX+SizeTextH, PosSrcY+SizeTextW );
    int iPosDestX = TextDestRect.left;
    int iPosDestY = TextDestRect.top;
    if ( (TextDestRect.right-TextDestRect.left)>SizeTextH )
        iPosDestX = iPosDestX+(TextDestRect.right-TextDestRect.left-SizeTextH)/2;
    if ( (TextDestRect.bottom-TextDestRect.top)>SizeTextW )
        iPosDestY = iPosDestY + (TextDestRect.bottom-TextDestRect.top-SizeTextW)/2;
    TextDestRect.left = iPosDestX;
    TextDestRect.top = iPosDestY;
    TextDestRect.right = TextDestRect.left+SizeTextH;
    TextDestRect.bottom = TextDestRect.top+SizeTextW;
    pMainBitmap->Canvas->DrawBitmap( pBitmapText, TheTextRect, TextDestRect, 100, true );

    delete( pBitmapText );
}
3
NGLN On

I assume this is because the text will rotate around the rectangle's TopLeft position

No, the rotation is centered around the current origin of the canvas. By default, that is coordinate 0, 0, but could be altered by the currently set deformation matrix. The typical way to go is: choose a rotation center, move the origin to that center, rotate, move back to the changed origin, and then draw. See TControl.MatrixChanged for reference. But there are many other ways.

Hereby an example of how to paint text from the lower left to the upper right within a form:

enter image description here

procedure TForm1.FormPaint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  Angle: Single;
  R: TRectF;
  S: String;
  H: Single;
  Matrix: TMatrix;
begin
  Canvas.Fill.Color := TAlphaColors.Black;
  Angle := -ArcTan2(ClientHeight, ClientWidth);
  R := ClientRect;
  S := 'Text from bottom-left...';
  H := Canvas.TextHeight(S);
  Matrix := CreateRotationMatrix(Angle);
  Matrix.m31 := Sin(Angle) * (ClientHeight - H);
  Matrix.m32 := ClientHeight  * (1 - Cos(Angle));
  Canvas.SetMatrix(Matrix);
  Canvas.FillText(R, S, False, 1, [], TTextAlign.taLeading,
    TTextAlign.taTrailing);
  S := '...to top-right';
  Matrix.m31 := ClientWidth * (1 - Cos(Angle)) + Sin(Angle) * H;
  Matrix.m32 := -Sin(Angle) * ClientWidth;
  Canvas.SetMatrix(Matrix);
  Canvas.FillText(R, S, False, 1, [], TTextAlign.taTrailing,
    TTextAlign.taLeading);
end;

Update:

This code does not yet take an already shifted origin into account.

In response to your comment, the following code draws text from coordinate 50, 100 down, 90° rotated around that point, using the method explained above, on a PaintBox which is arbitrarily positioned on the form.

procedure TForm1.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
const
  S = 'Hello World';
var
  R: TRectF;
  OriginalMatrix: TMatrix;
  ShiftMatrix: TMatrix;
  RotationMatrix: TMatrix;
  ShiftBackMatrix: TMatrix;
  Matrix: TMatrix;
begin
  PaintBox1.Canvas.Fill.Color := TAlphaColors.Black;
  R.Right := 50;
  R.Bottom := 100;
  R.Left := R.Right - 5000;
  R.Top := R.Bottom - 5000;
  OriginalMatrix := PaintBox1.Canvas.Matrix;
  ShiftMatrix := IdentityMatrix;
  ShiftMatrix.m31 := -R.Right;
  ShiftMatrix.m32 := -R.Bottom;
  RotationMatrix := CreateRotationMatrix(DegToRad(-90));
  ShiftBackMatrix := IdentityMatrix;
  ShiftBackMatrix.m31 := R.Right;
  ShiftBackMatrix.m32 := R.Bottom;
  Matrix := MatrixMultiply(RotationMatrix, ShiftBackMatrix);
  Matrix := MatrixMultiply(ShiftMatrix, Matrix);
  Matrix := MatrixMultiply(Matrix, OriginalMatrix);
  PaintBox1.Canvas.SetMatrix(Matrix);
  PaintBox1.Canvas.FillText(R, S, False, 1, [], TTextAlign.taTrailing,
    TTextAlign.taTrailing);
  PaintBox1.Canvas.SetMatrix(OriginalMatrix);
end;

Which can be reduced to:

procedure TForm1.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
const
  S = 'Hello World';
var
  R: TRectF;
  SaveMatrix: TMatrix;
  Matrix: TMatrix;
begin
  PaintBox1.Canvas.Fill.Color := TAlphaColors.Black;
  R := RectF(-Canvas.TextWidth(S), -Canvas.TextHeight(S), 0, 0);
  SaveMatrix := PaintBox1.Canvas.Matrix;
  Matrix := CreateRotationMatrix(DegToRad(-90));
  Matrix.m31 := 50;
  Matrix.m32 := 100;
  PaintBox1.Canvas.MultyMatrix(Matrix);
  PaintBox1.Canvas.FillText(R, S, False, 1, [], TTextAlign.taTrailing,
    TTextAlign.taTrailing);
  PaintBox1.Canvas.SetMatrix(SaveMatrix);
end;

Which in turn evolves into this general routine:

procedure DrawRotatedText(Canvas: TCanvas; const P: TPointF; RadAngle: Single;
  const S: String; HTextAlign, VTextAlign: TTextAlign);
var
  W: Single;
  H: Single;
  R: TRectF;
  SaveMatrix: TMatrix;
  Matrix: TMatrix;
begin
  W := Canvas.TextWidth(S);
  H := Canvas.TextHeight(S);
  case HTextAlign of
    TTextAlign.taCenter:   R.Left := -W / 2;
    TTextAlign.taLeading:  R.Left := 0;
    TTextAlign.taTrailing: R.Left := -W;
  end;
  R.Width := W;
  case VTextAlign of
    TTextAlign.taCenter:   R.Top := -H / 2;
    TTextAlign.taLeading:  R.Top := 0;
    TTextAlign.taTrailing: R.Top := -H;
  end;
  R.Height := H;
  SaveMatrix := Canvas.Matrix;
  Matrix := CreateRotationMatrix(RadAngle);
  Matrix.m31 := P.X;
  Matrix.m32 := P.Y;
  Canvas.MultyMatrix(Matrix);
  Canvas.FillText(R, S, False, 1, [], HTextAlign, VTextAlign);
  Canvas.SetMatrix(SaveMatrix);
end;

procedure TForm1.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
begin
  PaintBox1.Canvas.Fill.Color := TAlphaColors.Black;
  DrawRotatedText(PaintBox1.Canvas, PointF(50, 100), DegToRad(-90),
    'Hello world', TTextAlign.taTrailing, TTextAlign.taTrailing);
end;