When writing Cross-Library Code, Helpers can be your friend

Since XE2, I have been writing cross-platform (Windows, OSX, iOS, and Android) and “cross-library” (VCL and FMX) Delphi components.  My goal has always been to write as little unique platform or library code as possible (to reduce bugs and maintenance).  Sadly, Embarcadero (or Idera or whoever owns Delphi this week) did not put much emphasis on making code portable between VCL and FMX at first, though this has been improving with each version of Delphi since XE2.

For example, TVector, TMatrix and TPointF were not originally in the common run-time library (RTL) but in FMX only, which made no sense for code that had no library dependencies.  But though Embarcadero has been improving the RTL greatly, what about visual code?  If you use the end-point controls (TListBox, TEdit, etc), it is possible to mostly write code that uses them that would work for both VCL and FMX (e.g., ListBox1.Items.Add(‘Hello’)).  However, if you have to dive into using the Canvas, there has not been much, if any, progress made there.

For example, to write TPaintBox.OnPaint code for VCL, you might write code like this:

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
 aCanvas: TCanvas;
begin
 aCanvas := (Sender as TPaintBox).Canvas;
 aCanvas.Font.Size := 20;
 aCanvas.TextOut(200,200,'Hello World!');
 aCanvas.Brush.Color := clRed;
 aCanvas.Pen.Width := 1;
 aCanvas.Rectangle(Rect(10,10,100,100));
 aCanvas.Brush.Color := clGreen;
 aCanvas.Pen.Width := 1;
 aCanvas.Ellipse(Rect(250,250,400,500));
 aCanvas.DrawFocusRect(Rect(250,250,400,500));
end;

Transferring this code to an FMX TPaintBox.OnPaint, we get a whole slew of errors:

[dcc32 Error] Unit2.pas(34): E2003 Undeclared identifier: 'TextOut'
[dcc32 Error] Unit2.pas(35): E2003 Undeclared identifier: 'Brush'
[dcc32 Error] Unit2.pas(35): E2003 Undeclared identifier: 'clRed'
[dcc32 Error] Unit2.pas(36): E2003 Undeclared identifier: 'Pen'
[dcc32 Error] Unit2.pas(37): E2003 Undeclared identifier: 'Rectangle'
[dcc32 Error] Unit2.pas(38): E2003 Undeclared identifier: 'Brush'
[dcc32 Error] Unit2.pas(38): E2003 Undeclared identifier: 'clGreen'
[dcc32 Error] Unit2.pas(39): E2003 Undeclared identifier: 'Pen'
[dcc32 Error] Unit2.pas(40): E2003 Undeclared identifier: 'Ellipse'
[dcc32 Error] Unit2.pas(41): E2003 Undeclared identifier: 'DrawFocusRect'

Unfortunately, the FMX TCanvas class does not have all the methods or properties that we have been used to with the VCL TCanvas.  At this point, what we usually do is start the laborious process of converting the code to FMX equivalents, e.g., Rectangle => FillRect, Brush => Fill, etc.  However, with class helpers we can make this process much less painful (though not always completely painless)

What are Class (Record) Helpers?

So what are class (record) helpers?  As the documentation states,

A class or a record helper is a type that - when associated with another class or a record - introduces additional method names and properties that may be used in the context of the associated type (or its descendants). Helpers are a way to extend a class without using inheritance, which is also useful for records that do not allow inheritance at all.

What this means is that it is a way to attach new methods and properties (but not new fields) to a class or record that can be used by calling code as if the new methods and properties were defined when the class was built.  These new methods and properties apply to the original class as well as any descendant classes as long as the class helper is in scope.

For example, to extend the VCL TBitmap class to have a Clear method, similar to the FMX TBitmap.Clear method, you can declare a class helper like this:

TBitmapHelper = class helper for TBitmap
public
  procedure Clear<span class="br0">(</span><span class="kw1">const</span> AColor<span class="sy1">:</span> TColor<span class="br0">)</span><span class="sy1">;</span> <span class="kw1">overload</span><span class="sy1">;
</span>  procedure Clear<span class="br0">(</span><span class="kw1">const</span> AColor<span class="sy1">:</span> TAlphaColor<span class="br0">)</span><span class="sy1">;</span> <span class="kw1">overload</span><span class="sy1">;
end;

[...]
procedure TBitmapHelper.Clear(const AColor: TColor);
begin
  Brush.Color := AColor;
  Brush.Style := bsSolid;
  Rectangle(Rect(0, 0, Width, Height));
end;

</span><span class="sy1">procedure TBitmapHelper.Clear(const AColor: TAlphaColor);
begin
  Brush.Color := RGB(TAlphaColorRec(AColor).R, TAlphaColorRec(AColor).G, TAlphaColorRec(AColor).B);
  Brush.Style := bsSolid;
  Rectangle(Rect(0, 0, Width, Height));
end;</span>

By adding this class helper for VCL TBitmap, you can now write:

Bitmap.Clear(clBlack);

Careful readers will notice that I added a procedure Clear( const AColor: TAlphaColor ) method.  This method gets to the purpose of this post.  By specifying a method with the exact same signature as the FMX TBitmap.Clear, your code that clears bitmaps can be written exactly the same with no changes whatsoever.  It can be a literal copy-and-paste.

Class (Record) Helpers for cross-library compatibility

Class (Record) Helpers are not intended to extend classes where you “own” and can modify the class code.  It is better in those cases to directly add the new methods and properties to the class itself.  However, when you do not own the code, class (record) helpers can be your friend and help you write portable code between VCL and FMX.  They can provide an elegant way to help write and maintain cross-library code, by either

  • Simplifying your code base that use common classes by adding new methods and properties to one library to achieve parity with the other library
  • Or, Attaching the same functionality to both libraries using the same code base

I’ll briefly discuss the second point to get it out of the way as it is not the focus of this blog post.  Class (Record) Helpers can help you add the same functionality to both libraries from one code base.  For example, say you wanted to give your developers a way to add a watermark to any bitmap for both VCL and FMX.  You can use a class helper to define an AddWatermark method to the TBitmap, for both VCL and FMX.  By carefully writing the method implementation, the same code could be used for both libraries (and then use #INCLUDE to scope the code for each library as discussed briefly in the digression part of this blog post and this Stack Overflow question)

Now onto the first point.  By using class helpers you can simplify your code base significantly by ensuring method signature and functional parity for the most part between the two libraries. For example, in our TPaintBox example above, the code can be significantly simplified by creating TCanvas helpers that add VCL TCanvas drawing functions to the FMX TCanvas and vice-versa.  By adding functions like DrawFocusRect, you can make the FMX TCanvas have methods exactly like the VCL TCanvas.  In this file, I have made class helpers for TBrushStroke and TCanvas.  Adding this file to the uses clause for my FMX PaintBox application, the errors have been significantly reduced.

[dcc32 Error] Unit2.pas(35): E2003 Undeclared identifier: 'clRed'
[dcc32 Error] Unit2.pas(38): E2003 Undeclared identifier: 'clGreen'

The way I fix these types of errors is that I define a clxRed and clxGreen that map to clRed/clGreen in VCL and TAlphaColorRec.Red/Green in FMX.  By changing the VCL/FMX code to use these constants, the code now looks like this and compiles in both libraries without changes:

procedure TForm1.PaintBox1Paint(Sender: TObject);
var
 aCanvas: TCanvas;
begin
 aCanvas := (Sender as TPaintBox).Canvas;
 aCanvas.Font.Size := 20;
 aCanvas.TextOut(200,200,'Hello World!');
 <strong>aCanvas.Brush.Color := clxRed;</strong>
 aCanvas.Pen.Width := 1;
 aCanvas.Rectangle(Rect(10,10,100,100));
 <strong>aCanvas.Brush.Color := clxGreen;</strong>
 aCanvas.Pen.Width := 1;
 aCanvas.Ellipse(Rect(250,250,400,500));
 aCanvas.DrawFocusRect(Rect(250,250,400,500));
end;

Limitations of Class (Record) Helpers

Note that the example paint code above was carefully contrived to avoid the limitations of class (record) helpers and the differences between the two library architectures 🙂  The biggest problems are

  • Class Helpers have no fields you can add to the original class, so you cannot imitate some functionality.  For example, the VCL TCanvas has the MoveTo and LineTo methods.  The LineTo method draws a line from the previous MoveTo or LineTo call to a new point.  Since we cannot store the last point, we cannot imitate these methods.  However, at least in this case, we can use class helpers for both libraries to introduce methods to provide the equivalent functionality.  For example, by creating a DrawLine method that takes the start and end point and using that method in both VCL and FMX.
  • If the functionality doesn’t exist, you cannot imitate it.  For example, the VCL TBrush has the Style property that specify different fill patterns.  You could sort of imitate this behavior by setting the FMX Fill.Kind to TBrushKind.Bitmap and using a bitmap as the pattern but it is not the same.  In this case, I decided the effort wasn’t worth it.
  • Simple types can be challenging. For example, in FMX the canvas coordinates are singles, TPointF, and TRectF.  In VCL, they are Integer, TPoint, and TRect.  To get the center point in FMX, you would write code like MyPoint.X := (Rect.Right – Right.Left) / 2 + Rect.Left.  This code produces a single, which in VCL needs to rounded to truncated.  You CAN get around even this though with a little work.  In the RSGraphics.pas unit (FMX.RS.Graphics in FMX) that is in the RiverSoftAVG Common Classes Library, there are TCanvasPoint types that map to TPoint for VCL and TPointF for FMX.  There are also functions like CanvasDiv, which in FMX does the division above but in VCL uses a DIV to keep everything in Integer.  You can get the RSGraphics.pas unit by installing the RiverSoftAVG Charting Component Suite (or if you are an owner of any RiverSoftAVG product) as the RiverSoftAVG Common Classes Library  comes with it. 
  • The SCOPEDENUMS directive used in FMX can be a real headache.  Converting code from (VCL): Canvas.Brush.Style := bsSolid becomes in FMX: Canvas.Fill.Kind := TBrushKind.Solid (or Canvas.Brush.Kind if you are using my class helper).  You can get around it by defining an enumeration in VCL that looks like the FMX version, but it is an annoyance.
  • There can be only one class (record) helper per class or record.  This is the toughest limitation of all if you encounter it, and there is not a lot you can do about it.  If it is an Embarcadero/Idera class helper, you can copy and paste the class helper functionality into your own class helper, but if it is a third-party’s code, you may be out of luck.

Well, that is the end of another long blog post, but I hope you think it was worth it and that it can help you in your porting of code between VCL and FMX.

Happy CodeSmithing!

 

Leave a Reply

Your email address will not be published. Required fields are marked *