Streaming non-published TPersistent Properties – A Better Way

In my last blog post, I discussed how to stream non-published properties, most importantly how to stream non-published TPersistent descendant properties.  The solution I discussed (using a TComponent intermediary so that you can call Writer.WriteComponent) is what you see most commonly as the solution on the web.  It works for the most part, but, in my opinion, there are several problems with it:

  • Does not work with the ObjectBinaryToText/ObjectTextToBinary methods
  • Inserts a fake path into the property name stream that can cause problems in some edge cases (for example, see the Inner.Address property in the output below)
  • Frankly, the resulting stream is ugly 🙂
object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 TOuterComponent = Null
 Inner.Address = '456 Lonely Way Anywhere, FL 32006'
 Inner.Price = 85000.000000000000000000
 Inner.Date = 42339.000000000000000000
end

When I started trying to stream SVG documents into Delphi binary streams for the latest version of the RiverSoftAVG SVG Component Library, I ran hard into the brick wall of those edge cases I just mentioned.  🙁  Fortunately, there is a better way.  After a couple of days, I came up with this method, which I am really happy about.  I could kick myself though for using the old way for over a decade; I was never totally satisfied with it ( and intuitively, I knew it would cause me problems someday) but better late than never. 🙂

With a little spelunking through the TWriter/TReader code, you can see that the Delphi classes almost give us what you need.  There is a TWriter.WriteProperties public method.  There isn’t a TReader.ReadProperties equivalent.  There is a TReader.ReadProperty protected method.  There isn’t a TWriter public method for resetting the private FPropPath field to an empty string (we need this to make the property path name local to our TPersistent object).

However, with a judicious hack of the Delphi streaming system, and accessing some protected methods, we can create a nice, clean streaming system for non-published TPersistent objects.

The trick comes down to making the Delphi streaming system believe we are writing a collection.  When we write a collection to a stream, the TWriter class resets its FPropPath variable and allows us to write out “collection items” the way we want.

So what we end up with in our stream is we write out the vaCollection value type (using the protected TWriter.WriteValue method), and then write out our TPersistent properties inside list begin and end markers.  We need to crack the TReader to get access to the ReadProperty protected method and the TWriter to get access to the WriteValue protected method.  Here is what our new streaming code looks like:

type
 TReaderCrack = class(TReader);
 TWriterCrack = class(TWriter);

function ReadPersistent(Reader: TReader; Instance: TPersistent): TPersistent;
begin
 result := Instance;
 // read vaCollection
 Reader.CheckValue(vaCollection);
 // read list of items, in this case, only one, the top-level instance
 Reader.ReadListBegin;
 while not Reader.EndOfList do
 TReaderCrack(Reader).ReadProperty(Instance);
 Reader.ReadListEnd;
 Reader.ReadListEnd;
end;

procedure WritePersistent(Writer: TWriter; Instance: TPersistent);
begin
 // write out the Instance properties as a collection. This forces the writer
 // to zero out the property path
 TWriterCrack(Writer).WriteValue(vaCollection);
 // collection is a list of items
 // we are writing out a collection of one item, the Comparable object
 Writer.WriteListBegin;
 Writer.WriteProperties(Instance);
 Writer.WriteListEnd;
 Writer.WriteListEnd; // match vaCollection
end;

procedure THouse.DefineProperties(Filer: TFiler);
begin
 inherited DefineProperties(Filer);
 Filer.DefineProperty('NumOffers', ReadNumOffers, WriteNumOffers, NumOffers <> 0);
 Filer.DefineProperty('Comparable', ReadComparable, WriteComparable, True);
end;

procedure THouse.ReadComparable(Reader: TReader);
begin
 ReadPersistent(Reader, FComparable);
end;

procedure THouse.WriteComparable(Writer: TWriter);
begin
 WritePersistent(Writer, FComparable);
end;

The same trick can also be used to write out our non-published PurchaseHistory object list.

procedure THouse.ReadPurchaseHistory(Reader: TReader);
var
 Transaction: TTransaction;
begin
 PurchaseHistory.Clear;
 // read vaCollection
 Reader.CheckValue(vaCollection);
 while not Reader.EndOfList do
 begin
 Transaction := TTransaction.Create;
 PurchaseHistory.Add(Transaction);
 Reader.ReadListBegin;
 while not Reader.EndOfList do
 TReaderCrack(Reader).ReadProperty(Transaction);
 Reader.ReadListEnd;
 end;
 Reader.ReadListEnd;
end;

procedure THouse.WritePurchaseHistory(Writer: TWriter);
var
 i: Integer;
begin
 TWriterCrack(Writer).WriteValue(vaCollection);
 for i := 0 to PurchaseHistory.Count - 1 do
 begin
 Writer.WriteListBegin;
 Writer.WriteProperties(PurchaseHistory[i]);
 Writer.WriteListEnd;
 end;
 Writer.WriteListEnd; // match vaCollection
end;

Our final stream works with ObjectBinaryToText and ObjectTextToBinary.  The output of our stream is clean, and everything looks like normal, published properties:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 Comparable = <
 item
  Address = '456 Lonely Way Anywhere, FL 32006'
  Price = 85000.000000000000000000
  Date = 42339.000000000000000000
 end>
 PurchaseHistory = <
 item
  Buyer = 'Baker Building Co'
  Seller = 'Anderson'
  Price = 45000.000000000000000000
  Date = 28260.000000000000000000
 end
 item
  Buyer = 'Anderson'
  Seller = 'Smith'
  Price = 60000.000000000000000000
  Date = 32654.000000000000000000
 end
 item
  Buyer = 'Smith'
  Seller = 'Johnson'
  Price = 90000.000000000000000000
  Date = 38629.000000000000000000
 end>
end

Writing out a TList in this case is easy as every item in the list is a TTransaction.  For heterogeneous lists (i.e., lists that contain different class types), it is still possible with just a little extra work.  We would write out the item class name first.  On reading the stream, you would read the class name, find the class, create it, and then read its properties.  However, I will leave this as an exercise for the reader or perhaps as a future blog post. 🙂

I hope you found this useful.  I know I did.  The full code, including a test program, is available to download.

Happy Holidays and Merry CodeSmithing! 🙂

Streaming non-published TPersistent properties

The Delphi streaming system is a marvel of design.  For most cases, streaming components to and from any stream (memory, file, string) just works.  You publish the properties you want to stream when designing your class, and the streaming system takes care of reading and writing your published properties.  Even streaming non-published properties is relatively easy.  However, there is one non-published property type that is much more difficult than it should be: object properties descended from TPersistent.

For the purposes of this blog post, we are going to define a THouse component.  This component has published properties like Address and Price as well as three unpublished properties:

  • NumOffers (simple integer property recording the number of buy offers we have on the house)
  • Comparable (an object property descended from TPersistent that holds the properties of a house that we think is comparable to the current house)
  • PurchaseHistory (a TObjectList property of TTransaction objects that record the purchase history of the house)

Here is the Delphi code for THouse, TComparable, and TTransaction:

uses
  Classes, SysUtils, Generics.Collections;

type
  TTransaction = class(TPersistent)
  private
    { private declarations }
    FDate: TDateTime;
    FPrice: Single;
    FSeller: String;
    FBuyer: String;
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create; overload; virtual;
    constructor Create( aSeller, aBuyer: String; aPrice: Single; aDate: TDate ); overload;
    function Clone: TTransaction; virtual;
  published
    { published declarations }
    property Buyer: String read FBuyer write FBuyer;
    property Seller: String read FSeller write FSeller;
    property Price: Single read FPrice write FPrice;
    property Date: TDateTime read FDate write FDate;
  end;
  TTransactions = TObjectList;

  TComparable = class(TPersistent)
  private
    { private declarations }
    FDate: TDate;
    FPrice: Single;
    FAddress: String;
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create; overload; virtual;
    constructor Create( aAddress: String; aPrice: Single; aDate: TDate ); overload;
  published
    { published declarations }
    property Address: String read FAddress write FAddress;
    property Price: Single read FPrice write FPrice;
    property Date: TDate read FDate write FDate;
  end;

  THouse = class(TComponent)
  private
    { private declarations }
    FPrice: Single;
    FDateBuilt: TDate;
    FBathrooms: Single;
    FPurchaseHistory: TTransactions;
    FComparable: TComparable;
    FNumOffers: Integer;
    FBedrooms: Integer;
    FAddress: String;
    procedure SetComparable(const Value: TComparable);
    procedure SetPurchaseHistory(const Value: TTransactions);
  protected
    { protected declarations }
  public
    { public declarations }
    procedure Assign(Source: TPersistent); override;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property NumOffers: Integer read FNumOffers write FNumOffers;
    property Comparable: TComparable read FComparable write SetComparable;
    property PurchaseHistory: TTransactions read FPurchaseHistory write SetPurchaseHistory;
  published
    { published declarations }
    property Address: String read FAddress write FAddress;
    property Price: Single read FPrice write FPrice;
    property DateBuilt: TDate read FDateBuilt write FDateBuilt;
    property Bathrooms: Single read FBathrooms write FBathrooms;
    property Bedrooms: Integer read FBedrooms write FBedrooms;
  end;

Note that for the streaming system to work, it needs to be able to find the classes on reading and create them.  Therefore, the classes must be registered:

initialization
 RegisterClasses([THouse, TComparable, TTransaction]);
end.

If the THouse component is streamed as is, only the published properties are saved:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
end

Streaming non-published properties

To stream non-published properties, you override the TPersistent.DefineProperty method.  From the Delphi help:

By default, writing an object to a stream writes the values of all its published properties, and reading the object in reads those values and assigns them to the properties. Objects can also specify methods that read and write data other than published properties by overriding the DefineProperties method.

This process is fairly straightforward and, in general, the Delphi streaming classes (TFiler, TReader, and TWriter) make this easy to do.  There are 3 steps to defining any property to be published:

  1. Override the protected DefineProperties method and declare your non-published property
  2. Write a WriteXXX( Writer: TWriter ) method for each non-published property and write the property to the stream using TWriter.WriteXXX methods
  3. Write a ReadXXX( Reader: TReader ) method for each non-published property and read the property from the stream using TReader.ReadXXX methods

For the first step, the TFiler class provides two methods for “declaring” your non-published property and defining the reader and writer methods to use: DefineProperty and DefineBinaryProperty.  These methods accept the name of your “fake” property and two methods (one for reading, one for writing).  Finally, there is a parameter to tell the TFiler if the property should be written out.  Use this parameter to not write out properties that equal their defaults.  So, in our example to write out the NumOffers simple integer property, the DefineProperties method might look like this:

procedure THouse.DefineProperties(Filer: TFiler);
begin
  inherited DefineProperties(Filer);
  Filer.DefineProperty('NumOffers', ReadNumOffers, WriteNumOffers, NumOffers <> 0);
end;

Make sure you call the inherited DefineProperties method so that any ancestor classes can write our their fake properties.

Coding the read and write methods is easy for simple property types, the TReader and TWriter class provide many methods for simple types, such as TWriter.WriteInteger, TWriter.WriteBoolean and TWriter.WriteString.  The ReadNumOffers and WriteNumOffers methods look like:

procedure THouse.ReadNumOffers(Reader: TReader);
begin
  FNumOffers := Reader.ReadInteger;
end;

procedure THouse.WriteNumOffers(Writer: TWriter);
begin
  Writer.WriteInteger(NumOffers);
end;

Note that the TFiler class does the work of figuring out when to call your Reader and Writer methods so you don’t have to put in any special checks in your code.  For example, when reading the stream, when the Filer object detects your fake property name in the stream, it will position the stream and then call your Reader method.

Streaming non-published TPersistent properties

The Delphi streaming system starts to fail you when you want to write out non-simple published properties.  Unfortunately, the TWriter class does not have a WritePersistent method or something similar to write out our THouse.Comparable property.  Comparably, the TReader class lacks ReadPersistent method for reading these types back in.  So what are we to do?

If you search the web, you quickly see that there are 2 recommendations:

  1. Write the properties of the TPersistent object yourself (this is the worst way to do it as it is extremely tedious (as you have to write all the properties yourself), error prone (what happens if the class deletes a property at a later date?), and not maintainable (what happens if new properties are added to the class?  They are not streamed out).
  2. Create a TComponent wrapper class that has a published TPersistent property which you set to your TPersistent class and then stream the TComponent using Writer.WriteComponent.

The second method works and something similar is what I used for many years.  The idea is that you create a TComponent descendant whose only purpose is to read and write your TPersistent descendant class.  The Delphi streaming class has TWriter.WriteComponent and TReader.ReadComponent methods, which you can then use to stream your TPersistent property.

  TOuterComponent = class(TComponent)
  { Purpose: Special class for writing out TPersistent objects.  Allows the
    use of WriteComponent.  This component frees itself when it is loaded in order
    to avoid problems with TReader.  For TWriter, you should still free it }
  private
    FInner : TPersistent;
  protected
    procedure Loaded; override;
  public
  published
    property Inner : TPersistent Read FInner Write FInner;
  end;

{ TOuterComponent }

procedure TOuterComponent.Loaded;
begin
 inherited;
 {$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};
end;

procedure WritePersistent(Writer: TWriter; Instance: TPersistent);
// Saves a persistent object to a TWriter object
var
 Outer: TOuterComponent;
begin
 Outer := TOuterComponent.Create(nil);
 try
 Outer.Inner := Instance;
 // Write the outer object, it automatically writes the inner object
 Writer.WriteComponent( Outer );
 finally
 Outer.Free;
 end;
end;

function ReadPersistent(Reader: TReader; aObject: TPersistent): TPersistent;
// read a persistent object from a TReader object
var
 Outer: TOuterComponent;
begin
 Outer := TOuterComponent.Create(nil);
 Outer.Inner := aObject;
 // read the outer object, it automatically reads the inner object
 Reader.ReadComponent( Outer );
 result := Outer.Inner;
end;

With the TComponent class above and the two methods, code for writing a TPersistent property looks like this:

procedure THouse.DefineProperties(Filer: TFiler);
begin
 inherited DefineProperties(Filer);
 Filer.DefineProperty('NumOffers', ReadNumOffers, WriteNumOffers, NumOffers <> 0);
 Filer.DefineProperty('Comparable', ReadComparable, WriteComparable, True);
end;

procedure THouse.ReadComparable(Reader: TReader);
begin
 ReadPersistent(Reader, FComparable);
end;

procedure THouse.WriteComparable(Writer: TWriter);
begin
 WritePersistent(Writer, FComparable);
end;

In general, this is decent method for writing object properties.  It writes out all published properties of the TPersistent-descended class (even fake properties!) and does not need to change as your TPersistent class changes.

However, this method can quickly break down.  If you use the ComponentToStringProc and StringToComponentProc methods described in the Delphi Component To String example, it fails.  It can also fail in other specialized cases such as when you start nesting these properties (i.e., List of TPersistent which has TPersistent which as a list, etc).  Finally, quite honestly, it looks ugly to me in streams.  If you write out the stream as a string, the TPersistent-descended property does not look clean like the other properties:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 TOuterComponent = Null
 Inner.Address = '456 Lonely Way Anywhere, FL 32006'
 Inner.Price = 85000.000000000000000000
 Inner.Date = 42339.000000000000000000
end

Fortunately, there is an even better way.  However, as I seem to be constitutionally incapable of writing short blog posts, I will stop here and leave you a teaser.  In my next blog post, I will detail how to write a TPersistent-like  and TList-like properties “the better way” and which provide clean output that can be passed through the Component To String example:

object THouse
 Address = '123 Main St Anywhere, FL 32007'
 Price = 100000.000000000000000000
 DateBuilt = 28216.000000000000000000
 Bathrooms = 2.500000000000000000
 Bedrooms = 3
 NumOffers = 2
 Comparable = <
 item
 Address = '456 Lonely Way Anywhere, FL 32006'
 Price = 85000.000000000000000000
 Date = 42339.000000000000000000
 end>
end

For now, Happy Holidays and Happy CodeSmithing!

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!

 

XE8 and Radial Gradients in Windows FMX Applications

The Problem

(Update 2015-06-14: Note that the article below has been updated to set Radial Gradient Center Point and not GradientOriginOffset (or focal point) .  Setting GradientOriginOffset can skew the gradient and lead to incorrect results).

In Delphi XE8, Embarcadero broke (or perhaps more correctly, broke further) how radial gradients are rendered on Windows using their FMX.Canvas.D2D.pas file.  This issue affects anyone who uses a radial gradient and the center point/focal point to not be in the exact middle of the shape being rendered.  For the RiverSoftAVG SVG Component Library, SVG tend to define these types of gradients all the time so that is how I became aware of the regression.  Prior to XE8, the FMX.Canvas.D2D unit created a radial gradient brush whose center was modified by the Brush.Gradient.RadialTransform.RotationCenter.  This allowed the SVG library to correctly render radial gradients that had their center or focal point not equal to 50%, 50% (i.e., the center).  The screenshots below show the old pre-XE8 behavior:

Radial Gradient (Centered Origin 50%, 50%)

Radial Gradient (Centered Origin 50%, 50%)

Radial Gradient (Origin Down and Right 75%, 75%)

Radial Gradient (Origin Down and Right 75%, 75%)

In the screenshots above, we set up the top Ellipse’s gradient brushes using RotationCenter and copied the brush to the canvas to draw 2 additional ellipses:

procedure TForm24.PaintBox1Paint(Sender: TObject; Canvas: TCanvas);
var
 aSize: Single;
begin
 aSize := PaintBox1.Height*0.33;
 Canvas.Fill := Ellipse1.Fill;
 // draw upper left
 Canvas.FillEllipse(RectF(0,0,aSize,aSize), 1);
 // draw bottom right
 Canvas.FillEllipse(RectF(PaintBox1.Width-aSize,PaintBox1.Height-aSize,PaintBox1.Width,PaintBox1.Height), 1);
end;

The code from XE7 and before in the FMX.Canvas.D2D.pas looked like this:

rgradbrushprop.GradientOriginOffset := TD2D1Point2F(Point(0, 0));
 rgradbrushprop.Center := TD2D1Point2F(
 PointF(AGradient.RadialTransform.RotationCenter.X * RectWidth(ARect),
 AGradient.RadialTransform.RotationCenter.y * RectHeight(ARect)) + ARect.TopLeft);
 rgradbrushprop.RadiusX := RectWidth(ARect) / 2;
 rgradbrushprop.RadiusY := RectHeight(ARect) / 2;
 FTarget.CreateRadialGradientBrush(rgradbrushprop, nil, gradcol, ID2D1RadialGradientBrush(Result));

If you notice, Embarcadero set the center of the gradient to the center point of the rectangle (usually RotationCenter.Point := PointF(0.5, 0.5)) and translated the center point by the rectangle being drawn (by adding ARect.TopLeft). There were quite a few problems with this code:

  • First is a small cosmetic problem. Why was Embarcadero using RadialTransform.RotationCenter? Rotation Center seems a poor naming choice for the center of the gradient. Probably, the RadialTransform.Position would have been a better choice.
  • Why were they setting the Center of the gradient and not the GradientOriginOffset?
  • There is no means to set the radius of the gradient (and that is why the SVG library cannot set the radius of a gradient)

However, there were a couple of things sorta right about the code:

  • You could set the gradient focal point
  • The center of the gradient was specified by using a value from 0 to 1 and was translated by where the rectangle was, meaning that it scaled to the rectangle it was being drawn in. If you changed the rectangle size or location, the gradient would be drawn correctly.

In XE8, Embarcadero changed FMX.Canvas.D2D.pas to closely mirror their FMX.Canvas.GDIP.pas:

RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(TPointF.Create(0, 0));
 RadialGradBrushProp.Center := TD2D1Point2F(TPointF.Create(ARect.Width * 0.5, ARect.Height * 0.5));
 RadialGradBrushProp.RadiusX := ARect.Width / 2;
 RadialGradBrushProp.RadiusY := ARect.Height / 2;
 FTarget.CreateRadialGradientBrush(RadialGradBrushProp, nil, GradCol, ID2D1RadialGradientBrush(Result));
 UpdateBrushMatrix(Result, AGradient.RadialTransform.Matrix);

All modifications to the radial gradient now occur in the UpdateBrushMatrix which applies the AGradient.RadialTransform’s transformation matrix to the brush’s matrix. In some ways, this could be seen as an improvement as theoretically, you can modify the radial gradient how ever you like by using the RadialTransform.Matrix. However, there are some BIG problems with this approach:

  • You cannot set TTransformation.Matrix property directly. It is read-only. The TTransformation.Matrix property is generated by the class when you modify the Position, Scale, and Skew properties. But the Skew property is protected. You can hack the class to get to the Skew property but it is certainly not straightforward.
  • Even worse, you need to set the transformation matrix using absolute values, not proportional values between 0 and 1. To translate the gradient center point, you need to know the rectangle it will be drawn in the future to create the gradient brush. And because the gradient center was not offset by the ARect.TopLeft, you have to take that into account too.
  • Even worse than that, since you must use absolute values for the gradient, that gradient can only be used correctly with one rectangle or object. In addition, even if the gradient on a TEllipse or other shape is set correctly, if that TEllipse is moved or resized, the gradient is wrong. You cannot share that gradient with another rectangle.

The screenshots below show the problems with the new, XE8 behavior:

Radial Gradient in XE8 (Centered Origin 50%, 50%).  Note how the bottom right ellipse has lost its centered gradient because it is drawn in a rectangle that is not at the origin.

Radial Gradient in XE8 (Centered Origin 50%, 50%). Note how the bottom right ellipse has lost its centered gradient because it is drawn in a rectangle that is not at the origin.

Radial Gradient in XE8 (Origin Down Right 75%, 75%).  By clever manipulation of the transformation matrix, we set the focal point to (114,94), which works for the top Ellipse.  However, the same gradient does not work for either of the other ellipses since their rectangles are different.

Radial Gradient in XE8 (Origin Down Right 75%, 75%). By clever manipulation of the transformation matrix, we set the focal point to (114,94), which works for the top Ellipse. However, the same gradient does not work for either of the other ellipses since their rectangles are different.

Note that the FMX GDI+ never worked, but since that canvas was used rarely, this issue was ignored by the RSCL.

Interestingly, Embarcadero only made this change on Windows. Testing on OSX and Android reveals that the previous behavior, using RotationCenter, is still in effect. (Note XE8 is giving us problems deploying to iOS so we could not test its behavior)

What does this mean for the RiverSoftAVG SVG Component Library?

First, because there is no easy way to set the gradient center point or focal point correctly and even if we did, the behavior would break as soon as a SVG element was moved or resized, we are not going to change the code for how radial gradient’s are set at this time. Using the default Embarcadero XE8 DirectX2D code on Windows for radial gradient with a non-centered focal point, the gradient will be displayed incorrectly. On other platforms and earlier versions of Delphi, the radial gradient will work as well as it ever did.

The Solution (for Everyone)

However, you can get back proper rendering of Radial Gradients on Windows for your compiled applications by hacking the FMX.Canvas.D2D.pas file. Perform the following steps:

  • Copy FMX.Canvas.D2D.pas from Delphi’s source\fmx directory to your project’s directory
  • Modify the TCanvasD2D.CreateD2DGradientBrush method by replacing the entire “begin { Radial }” with
begin
 { Radial }
 for I := 0 to AGradient.Points.Count + Count - 1 do
 Grad[I].Position := 1 - Grad[I].Position;
 FTarget.CreateGradientStopCollection(@Grad[0], AGradient.Points.Count + Count, D2D1_GAMMA_2_2,
 D2D1_EXTEND_MODE_CLAMP, GradCol);
 // assume RotationCenter in range 0-1, modify the gradient origin offset
 RadialGradBrushProp.GradientOriginOffset := TD2D1Point2F(TPointF.Create(0, 0));
 // assume RotationCenter in range 0-1, translate gradient center by rectangle.TopLeft
 RadialGradBrushProp.Center := TD2D1Point2F(TPointF.Create(AGradient.RadialTransform.RotationCenter.X * ARect.Width,
 AGradient.RadialTransform.RotationCenter.Y * ARect.Height) + ARect.TopLeft);
 // bonus points, assume scale contains the percent of the radius to display
 // i.e., usually r=1 for the whole rectangle
 RadialGradBrushProp.RadiusX := AGradient.RadialTransform.Scale.X*(ARect.Width / 2);
 RadialGradBrushProp.RadiusY := AGradient.RadialTransform.Scale.Y*(ARect.Height / 2);
 FTarget.CreateRadialGradientBrush(RadialGradBrushProp, nil, GradCol, ID2D1RadialGradientBrush(Result));
// UpdateBrushMatrix(Result, M);
 GradCol := nil;
 end;

Alternatively, you can download the changed file.  As a bonus, the above code uses the AGradient.RadialTransform.Scale property as a proxy for setting the radius of the gradient (the RSCL has set the scale based on a SVG element’s radius since February 2015 but it is unused without the above hack for each platform and version of Delphi you are using).

Well, that’s it for now.  I hope this helps others.  Happy CodeSmithing!

Creating Icons and Launchers for Delphi Mobile Applications – Redux

About a year ago, I blogged about a handy little utility (EXE and source) I created for generating all of the mobile icons, spotlights, settings and launchers for Delphi Android and iOS applications.  At the time, there were 2 different platforms (iOS and Android), 3 different device types (iPhone, iPad, Android), and 7 different ratios (1:1, 1.33:1, etc) of icon/launcher graphic sizes all combining to require a whopping total of 28 different png files to be created.  GraphicsTab

Fast forward a year, there have been 2 updates to Delphi in the time (adding 2 more ratios and a total of 37 files).  Graham Murt kindly updated the utility for XE6/7 and added the great little feature of persisting your choices in an ini file between executions.  I extended his update a little bit for the Android Splash Images and persisting the rectangles as well and am re-releasing our changes back to you.   Please read the original blog post for instructions.  You can download the source and exe here.

Happy CodeSmithing!

Note: In some ways, this utility complements the XE Plus Pack – Release 17 that jed-software.com just released.  However, the utility takes another approach by using images you create in Photoshop or some other tool (allowing more flexibility) and then generates all the different ratios quickly and easily.

ZEROBASEDSTRINGS, Just Don’t

Ack!  I cannot believe that it has been 6 months since my last post!  I apologize for the long silence but I have been juggling 3 major upgrades (the RiverSoftAVG Charting Component Suite and 2 others I am not yet ready to blog about) and it has swamped my time completely.

This is a small blog post about ZEROBASEDSTRINGS, in my opinion a truly moronic change Embarcadero introduced in XE3 for mobile.  I know I have said that I feel sometimes like Embarcadero is out to get me, but here is a case where Embarcadero seems to be out to get everyone. 🙂  Jeroen provides an excellent little blog post about it and I recommend you read that first to understand the issue.

However, I would go farther than Jeroen.  My advice, never use ZEROBASEDSTRINGS. Even if you have only started using Object Pascal now and have no experience with 1 based strings.  There are millions of lines of Delphi code and examples out there, most of it based on the traditional 1-based strings.  You will use some of that code, if you haven’t already.  You will copy and paste some of that code.  Mixing and matching 0 based and 1 based strings is a recipe for disaster.  Eventually, if you do mobile, this change will bite you in the a**.  And it will probably be a very subtle bug that will take way too long to track down (I know I wasted a day on it 🙁 ).  The worst part is that your mobile app will work perfectly fine on desktop, where you probably do most of your testing.  It is only when you move to mobile that strange things will start happening.

The first thing I recommend you do whenever you create a new unit is add your company/copyright header and add a {$IFDEF NEXTGEN}{$ZEROBASEDSTRINGS OFF}{$ENDIF} right below it.  Alternatively, as a component developer, I have to support multiple Delphi versions.  As such, I have a RSDefines.inc file (defining things like DELPHIXE7ORHIGHER, etc) which is included in every unit.  In that unit, I have the following lines:

{$IFDEF NEXTGEN}
 {$ZEROBASEDSTRINGS OFF}
{$ENDIF}

You may want to do the same thing, which gets applied no matter which version of Delphi and which compiler (mobile, desktop, etc).

By just adopting this habit of always prefacing every unit with ZEROBASEDSTRINGS OFF, you never have to worry about whether you are using 0-based or 1-based strings.  You won’t have to surround any more of your code with IFDEFs or Lows/High as detailed in Jeroen’s post.

Of course, this is my opinion.  But, when it comes to ZEROBASEDSTRINGS, my advice is JUST DON’T.  Friends Don’t Let Friends Use Zero Based Strings 🙂 (in Delphi at least).  It will save you a lot of time and heartache later.

Happy CodeSmithing!

 

Innosetup and Appmethod

RiverSoftAVG.com uses Innosetup to create the installers for all of its products.  With the recent release of Appmethod, I wanted the installers to detect if it was RAD Studio or Appmethod installed on a user’s computer.  This way, I could make one installer for all versions of RAD Studio and Appmethod, but “personalize” it based on what was detected.  I don’t know how others are doing it but here is how I did it.

First, I inspected the registry after installing appmethod and realized that Embarcadero sets the ‘(Default)’ value of the ‘SOFTWARE\Embarcadero\BDS\xx.0\Personalities’ (where xx is the version number), under the HKEY_CURRENT_USER, to the installed product.  For example, ‘SOFTWARE\Embarcadero\BDS\12.0\Personalities’ contains ‘RAD Studio XE5’ ‘SOFTWARE\Embarcadero\BDS\14.0\Personalities’ contains ‘RAD Studio XE6’.  For Appmethod, ‘SOFTWARE\Embarcadero\BDS\14.0\Personalities’ contains ‘Appmethod’.  Now, Appmethod and RAD Studio XE6 cannot coexist on the same computer so all I need to do was look for ‘Appmethod’ or ‘RAD Studio XE6’ for that value.  I created a function in Innosetup called IsAppmethodInstalled to do that:

function IsAppmethodInstalled( SVers: String ): Boolean;
var
  Vers: Integer;
  i: Integer;
  DefApp: String;
begin
  // check if Delphi is installed
  // 50 = Delphi5, 60 = Delphi 6 etc,
  // can check for ANY of multiple versions of delphi if separated by spaces
  i := Pos(' ', SVers);
  if i &gt; 0 then
  begin
    result := IsAppmethodInstalled( Copy(SVers, i + 1, Length(SVers)) );
    if result then Exit;
    SVers := Copy(SVers, 1, i-1);
  end;
  Vers := StrToInt(SVers);
  case Vers of
    200: result := RegQueryStringValue( HKCU, 'SOFTWARE\Embarcadero\BDS\14.0\Personalities', '', DefApp ); // XE6/Appmethod 1.14
  else
    result := False;
  end;
  if result then
  begin
    result := DefApp = 'Appmethod';
  end;
end;

The SVers parameter contains the version string which tells the function which product to look for (though for now, there is only the one).  Basically, the function looks in the registry for the ‘(Default)’ value (which is a blank string), pulls it out if it exists, and then sees if it equals ‘Appmethod’. To use the function, I call the function as part of the check parameter when I am defining the setup [Components] in innosetup (note that I also have a similar IsDelphiInstalled function that just checks for the RegKeyExists for each version of Delphi, e.g., 200: result := RegKeyExists( HKCU, ‘SOFTWARE\Embarcadero\BDS\14.0’ );):

<strong>[Components]</strong>
Name: main; Description: Main Files; Types: full compact custom; Flags: fixed
Name: help; Description: Help Files; Types: full
Name: d14; Description: Delphi 2010/Rad Studio 2010 (Win32) Library Files; Types: full; check: IsDelphiInstalled('140');
Name: d15; Description: Delphi XE/Rad Studio XE (Win32) Library Files; Types: full; check: IsDelphiInstalled('150');
Name: d16; Description: Delphi XE2/Rad Studio XE2 (Win32/FMX) Library Files; Types: full; check: IsDelphiInstalled('160');
Name: d16x64; Description: Delphi XE2/Rad Studio XE2 (Win64) Library Files; Types: full; check: IsDelphiInstalled('160');
Name: d17; Description: Delphi XE3/Rad Studio XE3 (Win32/FMX) Library Files; Types: full; check: IsDelphiInstalled('170');
Name: d17x64; Description: Delphi XE3/Rad Studio XE3 (Win64) Library Files; Types: full; check: IsDelphiInstalled('170');
Name: d18; Description: Delphi XE4/Rad Studio XE4 (Win32/FMX) Library Files; Types: full; check: IsDelphiInstalled('180');
Name: d18x64; Description: Delphi XE4/Rad Studio XE4 (Win64) Library Files; Types: full; check: IsDelphiInstalled('180');
Name: d19; Description: Delphi XE5/Rad Studio XE5 (Win32/FMX) Library Files; Types: full; check: IsDelphiInstalled('190');
Name: d19x64; Description: Delphi XE5/Rad Studio XE5 (Win64) Library Files; Types: full; check: IsDelphiInstalled('190');
Name: AM20; Description: Appmethod Library Files; Types: full; check: IsAppmethodInstalled('200');
Name: d20; Description: Delphi XE6/Rad Studio XE6 (Win32/FMX) Library Files; Types: full; check: IsDelphiInstalled('200') and not IsAppmethodInstalled('200');
Name: d20x64; Description: Delphi XE6/Rad Studio XE6 (Win64) Library Files; Types: full; check: IsDelphiInstalled('200') and not IsAppmethodInstalled('200');

After that, I can use the Components parameter ‘AM20’ to control what I install on the user’s computer.  That’s it.

Hopefully this will help other component developers who use Innosetup.  Take care for now and Happy CodeSmithing!

Of Paranoia and Enumerated Types

They say that just because you are paranoid, it doesn’t mean that someone is not out to get you.  I have never been paranoid, but I think I might start, as I think someone at Embarcadero is out to get me… or at least, out to get third party developers who create components for FMX 🙂  Supporting FMX components is simple masochistic fun, especially when you try to develop cross-library components (i.e., support VCL and FMX).  When I get especially frustrated, I start imagining that there is someone at Embarcadero, laughing maniacally every time a new release of Delphi and FMX come out.  It is hard to annoy your developer community as well as Embarcadero has unless you are truly trying. 🙂

Each version, Embarcadero likes to mix things up as they work to make FMX better, giving FMX component developers a perpetual thrill ride.  Not only do FMX component developers have to deal with bugs and slow performance, we have the joy of managing the constantly changing hierarchy, modified method names, and changed method signatures. Sometimes, seemingly just for giggles, types and classes will move to new units, giving FMX developers a fun whack-a-mole, find-that-class game with each release.  Supporting multiple versions of FMX from one code base is an IFDEF heaven. 🙂

All of which leads up to the point of this rant, I mean post, and one of my new pet peeves.  Enumerated types (and sets) are one of my favorite features of the Pascal language.  They are a joy to use: elegant and descriptive, beautifully type-safe while providing amazing power, especially with sets.  They are one of the features I miss the most when I have to go use another language.  However, it seems like Embarcadero is trying to ruin them for me, at least in FMX.

In FMX, every enumerated type is compiled with the SCOPEDENUMS directive on.  For those who don’t know, when an enumerated type is compiled with SCOPEDENUMS on, you have to type the enumerated type when you use it, e.g.,  this is not valid

MyControl.Align := alClient;

Instead, you are required to type:

MyControl.Align := TAlignLayout.alClient;

In my opinion, SCOPEDENUMS are one of the stupidest ideas that Embarcadero has ever adopted.  I know, I know, other languages have the same thing.  Well guess what, I like that you didn’t have to do that in Pascal.  What is the point of having a strongly-typed language if you have to do all the work for the compiler anyway?

For someone who tries to create code that works in both FMX and VCL, the SCOPEDENUMS directive is especially trying.  Either you have to type:

MyControl.Align := {$IFDEF FMX}TAlignLayout.{$ENDIF}alClient;

or

{$IFDEF FMX}
  TMyAlign = TAlignLayout;
{$ELSE}
  TMyAlign = TAlign;
{$ENDIF}
MyControl.Align := TMyAlign.alClient;

Digression: Note that the {$DEFINE FMX} is something I define to say if I am compiling for FMX or VCL., similar to what is discussed here.  In practice, I have one unit called FMX.RS.Something.pas that is defines the FMX and then includes the common unit, e.g.,

unit FMX.RS.Charts;
{$DEFINE FMX}
{$INCLUDE RSCharts.pas}

I found this solution on the forums one day, found the idea elegant, and use it extensively to make all RiverSoftAVG.com cross-library components.

But back to my rant… Embarcadero’s practice of using SCOPEDENUMS for FMX caused a lot of unnecessary code in my libraries.  I found the decision dumb, but I did the work, I could get around it, and life was good again…

…Until Delphi XE6.  In Delphi XE6, someone at Embarcedero decided to up the dumb level to 11.  🙁  In Delphi XE6, FMX enumerated types do not include the prefix!  TAlignLayout is no longer:

TAlignLayout = (alNone, alTop, alLeft, alRight, alBottom, alMostTop, alMostBottom, alMostLeft, alMostRight, alClient,alContents, alCenter, alVertCenter, alHorzCenter, alHorizontal, alVertical, alScale, alFit, alFitLeft, alFitRight);

but this:

 TAlignLayout = (None, Top, Left, Right, Bottom, MostTop, MostBottom, MostLeft, MostRight, Client, Contents, Center, VertCenter, HorzCenter, Horizontal, Vertical, Scale, Fit, FitLeft, FitRight);

 TAlignLayoutHelper = record helper for TAlignLayout
 const
 alNone = TAlignLayout.None deprecated 'Use TAlignLayout.None';
 alTop = TAlignLayout.Top deprecated 'Use TAlignLayout.Top';
 alLeft = TAlignLayout.Left deprecated 'Use TAlignLayout.Left';
 alRight = TAlignLayout.Right deprecated 'Use TAlignLayout.Right';
 alBottom = TAlignLayout.Bottom deprecated 'Use TAlignLayout.Bottom';
 alMostTop = TAlignLayout.MostTop deprecated 'Use TAlignLayout.MostTop';
 alMostBottom = TAlignLayout.MostBottom deprecated 'Use TAlignLayout.MostBottom';
 alMostLeft = TAlignLayout.MostLeft deprecated 'Use TAlignLayout.MostLeft';
 alMostRight = TAlignLayout.MostRight deprecated 'Use TAlignLayout.MostRight';
 alClient = TAlignLayout.Client deprecated 'Use TAlignLayout.Client';
 alContents = TAlignLayout.Contents deprecated 'Use TAlignLayout.Contents';
 alCenter = TAlignLayout.Center deprecated 'Use TAlignLayout.Center';
 alVertCenter = TAlignLayout.VertCenter deprecated 'Use TAlignLayout.VertCenter';
 alHorzCenter = TAlignLayout.HorzCenter deprecated 'Use TAlignLayout.HorzCenter';
 alHorizontal = TAlignLayout.Horizontal deprecated 'Use TAlignLayout.Horizontal';
 alVertical = TAlignLayout.Vertical deprecated 'Use TAlignLayout.Vertical';
 alScale = TAlignLayout.Scale deprecated 'Use TAlignLayout.Scale';
 alFit = TAlignLayout.Fit deprecated 'Use TAlignLayout.Fit';
 alFitLeft = TAlignLayout.FitLeft deprecated 'Use TAlignLayout.FitLeft';
 alFitRight = TAlignLayout.FitRight deprecated 'Use TAlignLayout.FitRight';
 end;

What’s the big deal you say?  After all, they provide backwards compatibility for using alClient, etc?  Ignoring that they just added a bunch of extra code for no reason (possibly bloating EXE sizes) and just wasted a record helper in order to provide backward compatibility for something they just broke, the big deal to me is that if you use TAlignLayout.alClient, you get the stupid deprecated warning, and warnings and hints are kind of a no-no when you are trying to develop quality components (for some reason, people get anxious 🙂 ).  A possibly even BIGGER DEAL (your mileage may vary) is that they broke compatibility with streaming in earlier versions of Delphi!  Once a form is saved in Delphi XE6, any alClient, alLeft, alRight, etc are converted to Client, Left, Right, etc making the form unusable in earlier versions of Delphi.  Aarrgh!  There are some truly choice words I said when I realized I completely broke a dialog in the IECS by saving it in Delphi XE6 (thankfully, I keep weekly backups).

Anyway, glad to get that off my chest.  Now, I am not truly paranoid, and I know that Embarcadero is not truly out to get me.  Eventually, my hope is that they will get FMX mature enough that you don’t see such core, breaking changes occurring each Delphi version.  I know in the grand scheme of things, I would rather have a stable, fast, working FMX than for them to worry about little ol’ developers like me.  However, if I ever walk past one of Embarcadero’s offices and hear an evil chuckle from within, I am not going to be held responsible for my actions.  🙂

Thank you for reading my rant.  Comments, as always, are welcome.

Happy CodeSmithing!

Performance Comparison from Delphi 2010 to XE6 PDF

The Performance Comparison from Delphi 2010 to XE6 series of blog posts has easily been the most popular ones I have ever done.  However, I know that it can be difficult to read as the blogging format is not really the best format for what is essentially an article.  I received a couple requests for a PDF version of the blog posts.  If you want to read the series as one big PDF (59 pages), you can download it here: Performance Comparison from Delphi 2010 to XE6.  It has been lightly edited from the blog to enhance the clarity and continuity.

Enjoy!  Happy CodeSmithing!

Performance Comparison from Delphi 2010 to Delphi XE6 (Conclusion)

The Last Chapter

With the release of Delphi XE6 and Embarcadero’s emphasis on Quality, Performance, and Stability (QPS), I wanted to see for myself the level of improvement, especially in performance.  Delphi XE6 is definitely faster and more responsive than the last few versions, especially in FMX, but I wanted to see if I could quantify the performance improvement. Before starting, I made some predictions about what I would see when comparing Delphi 2010-XE6.  Let’s see how I did:

  • EXE size will probably increase with every version of Delphi.  This turned out to be mostly true.  Each version of Delphi has increased the EXE size over the one before… EXCEPT this ignores the one proverbial elephant in the room, Delphi XE3.  Delphi XE3 massively increased EXE sizes, both in VCL and FMX.  Thankfully, Delphi XE4 reduced XE3’s excessive weight gain.  However, EXE size has been growing back to XE3 levels with every version since then.  It should be noted that VCL EXE sizes have been growing slower than FMX EXE sizes.
  • FMX executables will be larger than VCL executables.  This was definitely true.  This was completely expected as FMX controls are non-native controls (i.e., they don’t use OS level equivalents) so all the drawing and interaction code must be compiled into the executable.
  • FMX executables will be slower than VCL executables, though each new version of Delphi for a platform should improve.  This was mostly true.  Except for where the VCL version is using a software renderer for heavy graphics drawing vs the FMX hardware renderer, the FMX versions are slower than their VCL counterparts.  There was one outlier, when strings are added to a TMemo and BeginUpdate/EndUpdate have not been called, the Delphi XE6 FMX version was faster than the VCL feature.  However, the discrepancy is easily explained as the VCL TMemo draws every string as it is added while the FMX TMemo does not.  The part that is not completely true is that each new version of Delphi would be improving the FMX speed.  Speeds were all over the place, and earlier versions were sometimes significantly faster.  Delphi XE6 is definitely faster than XE5 though.
  • Win32 and Win64 compilation should be faster than other platforms.  This was true as well.  The Win32 and Win64 compilers (written by Embarcadero) are fastest.  However, the OSX version (also written by Embarcadero) is very close.  The iOS and Android versions are much slower, even when not counting the deployment step.
  • Windows FMX executables will be faster than other platforms’ FMX executables.  This was mostly true to true.  Since the test machines were not comparable, it was difficult to really test this.  However, when drawing the RSCL directly to the canvas, the OSX implementation was comparable to the Win32 implementation.  With the exception of the Clock, the OSX versions cleaned the Win32 versions clocks, being anywhere from 1.2x-4.2x faster.  It must be noted that the Windows box (Windows 7 64-bit Intel I7 930 @ 2.8 GHz CPU, ATI HD 5700 graphics card, and 6 GB RAM) and the Mac Mini (OSX 64-bit Intel I7 @ 2.3 GHz, integrated graphics, and 4GB RAM) are not exactly comparable.  Admittedly, the ATI HD 5700 is almost 5 years old now, but the Mac Mini uses integrated graphics.  Examining the outputs closely, there are slight differences in the output (see the dark lines in the Car).  Generally, however, the output from the OSX versions are excellent.  This hypothesis would need to be more rigorously investigated.

Comparing the competitors…

To make my final evaluation, I wanted to be as objective as possible and use concrete numbers.  I decided to ignore EXE size (Delphi 2010 wins! :-)) and compilation speed (more mixed but generally earlier versions of Delphi).  I concentrated solely on performance metrics.  This means I ignored features (Delphi XE6 wins!), number of platforms (Delphi XE6 wins), and stability (???? wins!) For each test application, I averaged the scores (e.g., the Hello World Listbox performance score is an average of the TListBox scores).  This does mean that the winner of an exponential test (e.g., 1000s of items in a TListBox) can win even if their performance with less items was not the best.  I then normalized the averages (e.g., the winner is equal to 1 and every other version is between 0 and 1 depending on their average).  Finally, I added each test score together to get a maximum score per platform.  For example, the VCL 32-bit score includes the normalized performance scores from the Hello World project (ListBox and Memo), the IECS Advanced Console and the IECS Basic Console (Max of 4 points).  The OSX score includes the normalized performance scores from the Hello World project (ListBox and Memo), the IECS Advanced Console, the IECS Basic Console, RSCL Drawing project, and the RSCL Primitives project (Max of 6 points).  In the graphs below, you can easily see how the normalized scores contributed to each Delphi version’s score. Note that my scores are just for the tests in this series of blog posts.  Other tests would give vastly different results.  This blog post is not meant to be a recommendation of one Delphi version over another.

Now for the awards ceremony…

Overall Performance Score Comparison, Win32 VCL

Overall Performance Score Comparison, Win32 VCL

Surprisingly, Delphi XE6 wins the gold award in my performance tests for Win32 VCL.  The differences are extremely minor between all versions of Delphi so this coveted award could have gone to any of them 🙂  Delphi XE3 manages second place and a silver medal.  Delphi XE scores the third place bronze award.

Overall Performance Score Comparison, Win64 VCL

Overall Performance Score Comparison, Win64 VCL

For Win64 VCL (dropping Delphi 2010 and XE from the running), Delphi XE2 manages to barely eke out the gold award.  Delphi XE6 wins silver and XE5 bronze.

Overall Performance Score Comparison, Win32 VCL

Overall Performance Score Comparison, Win32 FMX

Moving to FMX, the performance differences get much larger.  Delphi XE3, despite its huge EXE sizes and poor Memo performance, produces the fastest FMX executables on Windows in these tests to win a gold medal.  Its ListBox performance is what really stands out.  Delphi XE4 makes a very strong second place showing for silver.  Bronze goes to Delphi XE6.

Overall Performance Score Comparison, Win64 FMX

Overall Performance Score Comparison, Win64 FMX

Going to 64-bit, Delphi XE3 again wins the gold with Delphi XE4 getting the silver.  Delphi XE6 comes again in third place, though its score improved noticeably over 32-bit.

Overall Performance Score Comparison, OSX

Overall Performance Score Comparison, OSX

Switching to the Mac, Delphi XE4 wins its first gold medal.  Delphi XE6 sneaks past XE3 to win the silver.

Overall Performance Score Comparison, iOS

Overall Performance Score Comparison, iOS

Shifting to mobile, we are down to Delphi XE4, XE5, and XE6.  Delphi XE4 wins another gold medal for the iOS platform on its solid memo performance.  Delphi XE6 gets second place for the silver and XE5 trails for the bronze.

Overall Performance Score Comparison, Android

Overall Performance Score Comparison, Android

For our last platform, Android, Delphi XE6 wins handily, easily beating XE5.

Conclusion

So what does this mean?  How did Delphi XE6 perform on its mantra of “Quality, Performance, and Stability?”  As far as this series of performance blog posts are concerned, Delphi XE6 did neither as well as I hoped nor as bad as I feared.  It only outright won a platform twice (and one of those was by a whisker on Win32 VCL).  However, Delphi XE6 was a solid performer and competitive for all platforms.  Compared to its immediate successor (Delphi XE5), it is a huge advance in performance and nearly brings it back up to the levels of Delphi XE3 and XE4, both of which performed surprisingly well in FMX.

Overall Performance "Medal" Count (Gold=3, Silver=2, Bronze=1)

Overall Performance “Medal” Count (Gold=3, Silver=2, Bronze=1)

If you do a medal count of the different platforms and give 3 points for Gold, 2 points for Silver, and 1 point for Bronze, Delphi XE6 racks up the most points.  Even if you ignore the Android platform, Delphi XE6 still did 1 point better than XE4.  If you then also ignored the iOS platform, Delphi XE6 would still tie with XE3 at 9 points.  Considering that it adds both iOS and Android platforms, seems to have fewer issues compared to Delphi XE3 and XE4, and vastly superior performance compared to Delphi XE5, I would say that Embarcadero achieved their goal. Almost a month and over 12,000 words later, I have finally finished this series of blog posts on the performance comparison from Delphi 2010 to Delphi XE6.  I hope you have enjoyed them.  It was a ton of work, but I think it was worth it.  Happy CodeSmithing!