Extended TClipboard implementation for FMX and VCL (CCR.Clipboard)

I’ve just pushed to GitHub (first time, so be gentle…) an extended, multi-platform TClipboard implementation for newer Delphi versions:

https://github.com/chrisrolliston/CCR.Clipboard

Where the platform allows, supports delayed rendering, virtual files, change notifications, and inter-process TClipboard-based drag and drop. The code originates from the FMX TClipboard I published a few years back, though is much extended, and was refactored to support the VCL too (XE2+). For more info, check out the readme first…

https://github.com/chrisrolliston/CCR.Clipboard/blob/master/Readme.md

… followed by the wiki pages for discussion of individual features, together with known issues and limitations:

https://github.com/chrisrolliston/CCR.Clipboard/wiki

Disclaimer: supporting multiple FMX versions ain’t no fun, so if you come to try it in XE4 or whatever and have an issue, I may not be able to help you. Also, if you’re interested in drag and drop on OS X, consider using my code with any version lower than XE8 a ‘proof of concept’ only…

 

Advertisements

FMX TClipboard now supports iOS

I’ve just checked in a revision of my open source FMX TClipboard implementation that has an iOS backend. This supports the current rather than the FPC-based version of ‘Delphi for iOS’, however the Windows and OS X backends still compile with XE2 and above.

In essence, the new code wraps the native iOS clipboard API (UIPasteboard) and presents it in a fashion that closely follows the VCL clipboard interface, just like the existing Windows and OS X support did the same for desktop platforms. For some reason Apple in their wisdom decided to make the iOS clipboard API similar yet randomly different to the OS X one, so even though there’s not masses of code, it was a bit fiddly to implement. Anyhow, I’ve also knocked out a little demo similar to the previous desktop one:

TiOSClipboard demo

Using (say) Photos, you can copy an image to the clipboard and paste it into the demo. Conversely, you can from the demo itself copy either just the text entered, just the image, both the text and the image as two representations of the same clipboard item, or both the text and image as a custom clipboard format. The ‘List Formats on Clipboard’ button, as its name implies, then lists the formats currently on the clipboard. This is what I get after copying an image from Photos on the iOS simulator:

Format List

Technically, the iOS clipboard, like the OS X one, can have multiple items, each with multiple representations. Since the multiple-item concept doesn’t exist on Windows (indeed, it didn’t exist on OS X originally either), my class is only concerned with the first item, which is what most applications only bother with anyhow.

If you want the code, the SVN URL for it and a few other pieces is the following:

http://delphi-foundations.googlecode.com/svn/trunk/FMX%20Utilities/

The core files are now CCR.FMXClipboard.pas, CCR.FMXClipboard.Apple.pasCCR.FMXClipboard.iOS.pas, CCR.FMXClipboard.Mac.pas and CCR.FMXClipboard.Win.pas, and together they stand alone.

FMX anti-pattern: returning nil rather than raising an exception on an invalid state or arguments

In the Delphi RTL and VCL, a method called LoadFromFile or the like will raise an exception if the file doesn’t exist or is invalid. Similarly, if an object property getter implements a lazy-loading pattern (meaning, the value of the property will only be initialised when first needed), it will raise an exception if the situation is such that nothing valid can be loaded or initialised. This is pretty basic exception programming – when an error arises, the flow of the calling code is forcibly interupted with a clear account of what call was invalid. Imagine if (say) TBitmap.LoadFromFile didn’t raise an exception when the file name passed to it didn’t refer to a valid bitmap file – calling code could go happily on its way assuming a graphic has been loaded when it hasn’t. Likewise, what if the Items property getter for TList<T> just returned nil (or equivalent) with an out of bounds index? If T is a class type, then off-by-one errors in the calling code will end up with cryptic access violations later on that might be nowhere near where the erroneous call was actually made!

Alas, but such basic understanding of exception programming is not grasped by at least one developer on the FMX team. Repeatedly across the XE2, XE3 and now (I learn) XE4 releases, methods that an experienced Delphi programmer would expect to raise an exception when nothing valid can be returned do not. As soon as one case is fixed – e.g., the TFmxObject.Children property getter was done so in XE3, albeit implicitly – another one or two are added, and frankly, it needs to stop now. The latest cases I learn are the HScrollBar and VScrollBar property getters on TScrollBox – done properly in XE3, the anti-pattern has however been introduced in XE4, causing ‘random’ access violations in TMemo (the FMX TMemo inherits from TScrollBox).

Annoying FireMonkey buglet/oversight of the week

My personal FireMonkey buglet/oversight of the week is this: setting a TTextControl descendant’s Text property inside its constructor doesn’t do anything. For those who don’t know, TTextControl is the base class for things like TLabel, TListBoxItem and TExpander in FMX. Create custom descendants of these, then, and the following sort of code will not work:

type
  TMyExpander = class(TExpander)
  public
    constructor Create(const AOwner: TComponent;
      const AText: string); reintroduce;
  end;

constructor TMyExpander.Create(const AOwner: TComponent;
   const AText: string);
begin
  inherited Create(AOwner);
  Text := AText;
end;

The reason is quite simple: TTextControl sets a flag in Create that prevents changes to the Text property doing anything until that flag is reset in an override of AfterConstruction. In true ‘FM squared’ fashion, the code is nevertheless convoluted enough to give you RSI from having to press F7 so much in trying to track this down. Argh!!!!

XE3 and Subversion/Google Code

Just a small heads up, but it has come to my attention that the XE3 IDE’s Subversion (SVN) integration does not support the older, pre-v1.7 Subversion format, at least out of the box. While there are interop issues with recent versions of TortoiseSVN (the Subversion client I actually use) too, the XE3 IDE goes a step further by just hanging when the ‘Open From Version Control’ command is used against a repository in the older format.

While this in practice won’t bother many people, Google Code (a free-as-in-beer way to publish open source code) still uses Subversion v1.6… which means anything Delphi-related there whose author chose Subversion precisely because the Delphi IDE provides SVN support in the box may now look slightly foolish… Anyhow, this is all just a slightly roundabout way of saying I’ve added a ZIP of the sample code for my XE2 book here!

[Edit – my initial post was overly negative about TortoiseSVN (thanks to Robert Love and M J Marshall for correcting me in the comments). The issue is that TortoiseSVN v1.7x forces you to upgrade working copies from v1.6x to v1.7x, which means you can’t use both it and the XE or XE2 IDE interchangeably, assuming you haven’t modified the IDE’s behaviour to use the newer SVN DLLs. However, if you only use TortoiseSVN as your client, it can happily work with v1.6x servers. This though gives even less reason for the XE3 IDE to just hang!]

Writing a simple FireMonkey TListLayout implementation

In FireMonkey, the usual way to group controls is to use a ‘layout’ of some sort. Conceptually, a layout is just a container control with no visual appearance of its own, at least by default. This contrasts to something like a panel or group box, which is also a container but one that the user sees as such.

A few layout classes are provided with Delphi. The simplest is TLayout, which does nothing more than implement the basic defintion of a layout control; beyond it stands TScrollBox, TScaleLayout, TGridLayout and TFlowLayout. You can read about them in the help, though in a nutshell, they do the following –

  • TScrollBox shows scroll bars as its contents extend beyond its own visible client area (the scroll bars are auto-hidden if not needed)
  • TScaleLayout resizes its controls as it itself is resized
  • TGridLayout positions and sizes its constituent controls in fixed-sized cells
  • TFlowLayout positions controls like the words in a paragraph, first left to right (or right to left via a property setting), then top to bottom. Controls are then moved accordingly when the layout is resized.

While it isn’t a layout control strictly speaking, an honorary mention also goes to the FireMonkey TListBox, whose fixed-width items can host child controls.

That said, I was recently wanting a layout object to organise controls top aligned from from top to bottom. Unfortunately, none of the standard layout classes met my requirements, which were as thus:

  1. Each control should fill the parent’s client width, unless the standard Paddings and Margins properties indicate otherwise.
  2. However, a control’s height should be specific to the control.
  3. The first control would be located at the top of the parent, the second immediately below the first (perhaps with a standard gap), the third immediately below the second and so on.
  4. It should be easy to change the order of controls.
  5. Ideally a vertical scroll bar should show if the controls cannot fit.

Requirement (1) ruled out TFlowLayout, number (2) ruled out TListBox and TGridLayout, none of the requirements made TScaleLayout relevant, and only (5) could be serviced by TScrollBox. Writing a custom layout class for the task proved pretty easy however:

  1. Create a new package project, and set its ‘description’ to something appropriate under Project|Options, Description (e.g., ‘List Layout Control’).
  2. Add a new unit to the package, and add System.SysUtils, System.Classes and FMX.Types to the unit’s interface section uses clause.
  3. Following TFlowLayout and its peers, declare a class descending from TControl. Publish the usual FMX control properties inherited from (but not published by) the base class.
  4. Following TFlowLayout again, add a new published property called VerticalSpacing, typed to Single, and add overrides for the DoAddObject, DoInsertObject and DoRealign protected methods.
  5. Declare the usual Register procedure needed for registering a custom component with the IDE.

Following this, the unit’s interface section should look like this. I’ve also added a ComponentPlatforms attribute to say the class supports Windows and OS X (‘any’ target would be more exact to be honest):

uses
  System.SysUtils, System.Classes, FMX.Types;

type
  [ComponentPlatforms(pidWin32 or pidWin64 or pidOSX32)]
  TListLayout = class(TControl)
  strict private
    FVerticalGap: Single;
    procedure SetVerticalGap(const Value: Single);
  protected
    procedure DoRealign; override;
    procedure DoAddObject(AObject: TFmxObject); override;
    procedure DoRemoveObject(AObject: TFmxObject); override;
  published
    property Align;
    property Anchors;
    property ClipChildren;
    property ClipParent;
    property Cursor;
    property DesignVisible;
    property DragMode;
    property EnableDragHighlight;
    property Enabled;
    property Locked;
    property Height;
    property HitTest;
    property Margins;
    property Opacity;
    property Padding;
    property PopupMenu;
    property Position;
    property RotationAngle;
    property RotationCenter;
    property Scale;
    property TouchTargetExpansion;
    property VerticalGap: Single read FVerticalGap write SetVerticalGap;
    property Visible;
    property Width;
    property OnApplyStyleLookup;
    property OnDragEnter;
    property OnDragLeave;
    property OnDragOver;
    property OnDragDrop;
    property OnDragEnd;
    property OnClick;
    property OnDblClick;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnPainting;
    property OnPaint;
    property OnResize;
  end;

procedure Register;

The implementation of Register is as you would expect if you’ve ever written a custom VCL control, namely a simple call to RegisterComponents. DoAddObject and DoInsertObject then just call the inherited implementation before requesting the control realigns its children. Lastly, the VerticalGap property setter assigns the backing field before requesting a realignment too:

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

{ TListLayout }

procedure TListLayout.DoAddObject(AObject: TFmxObject);
begin
  inherited;
  Realign;
end;

procedure TListLayout.DoRemoveObject(AObject: TFmxObject);
begin
  inherited;
  Realign;
end;

procedure TListLayout.SetVerticalGap(const Value: Single);
begin
  if Value = FVerticalGap then Exit;
  FVerticalGap := Value;
  Realign;
end;

The final thing to implement is the DoRealign override. As a bit of an aside, DoRealign itself embodies the XE2 to XE3 FireMonkey transition (a lot of good work done, but a lot still to complete) in microcosm: in XE2, there was just Realign, which was a public, virtual method. As implemented in TControl, it did a whole load of checks to see whether controls should be realigned before finally doing the actual realigning. This was bad design, since if you wished to customise how realignment is performed in a descendant of TControl, you had to duplicate all those initial checks in your Realign override. In XE3, in contrast, Realign has been devirtualised and instead paired with a virtual, protected DoRealign method. In the new scheme, Realign still performs all the initial checks it did before, however it then delegates to DoRealign to do the actual repositioning and resizing. All well and good, but the refactoring wasn’t quite finished – to prevent the possibility of recursive calls to Realign/DoRealign, DoRealign still needs to set a protected FDisableRealign field to True, then reset it to False once it has finished. Really Realign should do that for you though, wrapping the FDisableAlign assignments in a try/finally block – if that were done, FDisableAlign could then be withdrawn into strict private scope. Nonetheless, it’s not a big issue.

Anyhow, here’s what my DoRealign implementation looks like:

procedure TListLayout.DoRealign;
var
  Control: TControl;
  NextY, StdWidth: Single;
begin
  if ControlsCount = 0 then Exit;
  FDisableAlign := True;
  try
    NextY := Margins.Top;
    StdWidth := Width - Margins.Left - Margins.Right;
    for Control in Controls do
      if Control.Visible then
      begin
        NextY := NextY + Control.Padding.Top;
        Control.SetBounds(Margins.Left + Control.Padding.Left,
          NextY, StdWidth - Control.Padding.Right -
          Control.Padding.Left, Control.Height);
        NextY := NextY + Control.Height +
          Control.Padding.Bottom + VerticalGap;
      end;
  finally
    FDisableAlign := False;
  end;
end;

If you’re following along, save everything, switch to the Release build configuration before adding and compiling for the Win64 and OS X target platforms (if you only have the Starter edition, that’s fine, however there won’t be any platforms beyond Win32 to compile for). The first time you compile the package there will be a prompt for adding fmx to the package’s requires clause – accept it. Then, toggle back to the default Win32 target, right click on the BPL’s node in the Project Manager, and choose Install. Finally, for each target platform, add the DCU output folder to the IDE’s search path (Tools|Options, Environment Options -> Delphi Options -> Library, Library Path); if desired, also add the .pas folder to the IDE’s browse path. E.g., if I were to save custom control units under E:\Delphi\Lib, this would give a default DCU output folder for 32 bit Windows of E:\Delphi\Lib\Win32\Release. If you toggle the project’s build configuration for Debug and recompile, you can also add the ..\Debug folders to the debug DCU search path as well. [In case it weren’t obvious, these instructions are in case you aren’t familiar with how to manually install a custom control – FMX or VCL – in the IDE. If you are, then there’s nothing particular to my example control, or shouldn’t be.] If all goes well, TListLayout should now be available in the Tool Palette when designing a form.

Now, the DoRealign implementation explicitly fulfils requirements (1) to (3) in my original list. Requirement (4) is also implicitly met, since our DoRealign lays out controls in the order they appear in the Controls array property, and that order can be changed by setting a sub-control’s Index as desired. So, if MyPanel is at the bottom of the layout control, setting its Index to 0 will move it to the top. Requirement (5) can then be met simply by nesting the TListLayout inside a TScrollBox, and setting its Align property to alTop:

TListLayout demo

Here, the form has a top-aligned TToolbar (StyleLookup set to ‘HeaderItemStyle’, as the default toolbar style looks pretty ugly IMO!), followed by a client-aligned TScrollbox with its Padding set to (2, 2, 2, 2). This then contains a top-aligned TListLayout with VerticalSpacing set to 4, with the layout itself containing three top-aligned panels, each of which has its Margins set to (8, 8, 8, 8), a left-aligned TLabel added and a client-aligned TEdit too.

How to survive without image lists in a FireMonkey project

In the VCL, toolbar icons (glyphs) are set up using the TImageList component. In FireMonkey however, TImageList does not exist, and even TToolBar itself is half the component it is in the VCL – rather than manage its own buttons, it’s just a simple container control like TPanel. To add buttons to a FMX TToolBar, you therefore drop TSpeedButtons onto it; and to add glyphs to the buttons, you add a TImage control to each one. On the face of it this leads to a simpler situation that had with the VCL; on the other though, it causes maintenance hassles in non-trivial applications if the same glyph is supposed to be used by different buttons, or if you come to want to change the icon set to a different one, given each image must be changed separately.

Being bothered about this issue, I’ve found relief is at hand from the FireMonkey styling system though. Let’s see it in action:

  • Create a new FireMonkey HD application.
  • Drop a TSpeedButton onto the form, and clear its Text property. Next, add a TPanel (sic.) to the button. Since TSpeedButton won’t let you do this directly, reparent the panel by dragging its node onto the button’s node in the Structure pane (top left).
  • Set the panel’s HitTest property to False, and its Align property to alClient. If you want to be able to drag the button around at designtime, set either the panel’s Padding property to (2, 2, 2, 2) or the button’s Margins property to (2, 2, 2, 2). Note the meaning of Padding and Margins is (weirdly) flipped round compared to the VCL – this is something I still haven’t got used to myself!
  • Add a TLayout to the form, and set its Visible property to False.
  • Add a TImage to the layout, load a suitable ‘Open File’ glyph into its Bitmap property, and set its StyleName to ‘FileOpenGlyph’. For ease of identification in the designer, set its Name property to ‘FileOpenGlyph’ too.
  • Set the panel’s StyleLookup property to ‘FileNewGlyph’. Note you’ll have to type it, as the name won’t be in the drop-down list.

At the end of all that, you should have something like this:

GlyphStyleDemo

How this works is that TPanel, as a styled control (TStyledControl descendant), gets its visual appearance from another control, or more exactly, a copy of another control – the ‘style’. This other control can be anything – all that is crucial is that its StyleName property is set to a value matched by the value of the StyleLookup property of the intended style subject(s).

Normally, custom styles are set up via a TStyleBook component. Using a style book means using the IDE’s FireMonkey style designer though, which is something only a programming masochist can enjoy. What isn’t so obvious is that you don’t actually need to use TStyleBook in the first place however – any control that has its StyleName property assigned will be available as a custom style. So, doing just that is what we did in the demo.

Now, we placed the source TImage on a hidden TLayout given we didn’t want it to be shown at runtime – setting its own Visible property to False wasn’t an alternative, since that would have caused the control it styled to be invisible at runtime too! However, it may be the case that you would prefer not to clutter a form with the source glyphs at all. In a VCL scenario, this would mean putting the application’s image lists onto a data module, but given TImage instances are controls, we can’t do that here. Instead, we need to put the glyph images onto a dummy form, auto-created but not ever visible. In my own case I had a further requirement: the ability to switch at runtime to a different glyph set. This was necessary because I’m looking to support both dark and light UI styles, and currently working with monochrome icons. While that may change later, there was no harm in ensuring everything would work if it didn’t.

So, continuing with the demo, add another form to the project, name it ‘StdGlyphsForm’, and save its unit as App.Glyphs.Std.pas. Then, go back to the first form, cut the TImage to the clipboard, then paste it onto StdGlyphsForm. Because we do not want the new form to become the application’s ‘main’ form in the VCL/FMX sense, do not have it auto-created in the normal fashion (Project|Options, Forms). Instead, it needs to be created explicitly when the application starts up, and directly (i.e., using TGlyphsForm.Create not Application.CreateForm).

Before we get to that though, let’s set up an alternative icon set to test with. To do that, save everything, then duplicate App.Glyphs.Std.pas via File|Save As…; save it as App.Glyphs.Alt.pas, then rename the form to AltGlyphsForm. After that, change the contents of the image, then add the original version of the unit (App.Glyphs.Std.pas) back to the project as well. In principle, we can now choose which icon set to use at runtime by instantiating the appropriate form class (StdGlyphsForm or AltGlyphsForm); if we wanted to then switch sets at runtime, we can just free the first glyphs form, instantiate the second, and ask all ‘normal’ forms to update their styling.

When targting OS X pursuing such a plan works fine, but when targeting Windows there’s a problem: specifically, at one point in FMX.Platform.Win.pas, Screen.Forms[0] is used in lieu of Application.MainForm when the latter cannot be guaranteed to be initialised yet. Because of this, the first form that gets created – regardless of how – becomes the ‘owner window’ for subsequent forms at the Windows API level. If the first form created were to be one of our glyph container forms, bad things will then result.

One way to get around this is to ensure the real ‘main form’ creates the glyph container. However, this assumes you have a ‘main’ form in the first place, which when targeting OS X especially isn’t necessarily the case. After fiddling about the problem, my preferred solution at present is to create the glyph container, move its contents to something else that isn’t a form too (just a bare TComponent instance will do), then immediately free it. So, let’s do that with the demo, wrapping the necessary code in a simple singleton type.

For this, add a new unit to the project, and call it App.Glyphs.pas. Then, copy the following code into its interface section:

uses
  System.Classes;

type
  TGlyphsStyle = (gsStandard, gsAlternative);

  GlyphsManager = record
  strict private class var
    FGlyphsOwner: TComponent;
    FGlyphsStyle: TGlyphsStyle;
    class constructor Initialize;
    class destructor Finalize;
    class procedure SetGlyphsStyle(Value: TGlyphsStyle); static;
  public
    class property GlyphsStyle: TGlyphsStyle read FGlyphsStyle write SetGlyphsStyle;
  end;

Next, make its implementation section look like this:

uses
  FMX.Types, FMX.Forms, FMX.Styles, App.Glyphs.Std, App.Glyphs.Alt;

class constructor GlyphsManager.Initialize;
begin
  FGlyphsOwner := TComponent.Create(nil);
  FGlyphsStyle := gsAlternative;
  SetGlyphsStyle(gsStandard);
end;

class destructor GlyphsManager.Finalize;
begin
  FGlyphsOwner.Free;
end;

class procedure GlyphsManager.SetGlyphsStyle(Value: TGlyphsStyle);
var
  Comp: TComponent;
  I: Integer;
  SourceClass: TComponentClass;
  Source: TComponent;
begin
  if Value = FGlyphsStyle then Exit;
  case Value of
    gsStandard: SourceClass := TStdGlyphsForm;
    gsAlternative: SourceClass := TAltGlyphsForm;
  else
    Assert(False);
    SourceClass := nil; //avoid compiler warning
  end;
  FGlyphsOwner.DestroyComponents;
  Source := SourceClass.Create(nil);
  try
    for I := Source.ComponentCount - 1 downto 0 do
    begin
      Comp := Source.Components[I];
      FGlyphsOwner.InsertComponent(Comp);
      if Comp is TFmxObject then
      begin
        TFmxObject(Comp).Parent := nil;
        if Comp is TControl then
          TControl(Comp).SetNewScene(nil); //see below
      end;
    end;
  finally
    Source.Free;
  end;
  FGlyphsStyle := Value;
  { next lines only necessary because we aren't updating
    the style as such as well }
  for I := Screen.FormCount - 1 downto 0 do
    Screen.Forms[I].UpdateStyle;
end;

The code here should be pretty straightforward – when the glyph set is to be changed, the old glyphs are first destroyed, the new ones loaded, and the application’s forms asked to refresh their and their controls’ styling. The only slightly tricky thing is how we change an existing component’s owner – since the Owner property is read-only, we need to call InsertComponent on the new owner instead, the implementation of which first deassigns any existing owner. Further, since a FireMonkey control, like a VCL one, has both a parent and an owner, and both will destroy the control when the parent or owner is itself destroyed, we need to clear the parent too. Lastly, due to an oversight in the FireMonkey source, a further reference – to the ‘scene’ – needs to be manually cleared as well. If you check out FMX.Forms.pas, you’ll find TCustomForm calls SetNewScene(Self) in DoAddObject, but not SetNewScene(nil) in DoRemoveObject. The resulting dangling reference then causes access violations if not cleared.

To put the new unit in action, head back to the main form of the demo (i.e., the one with the button on it), remove the now empty TLayout, and add a couple of radio boxes called rdoStandard and rdoAlternative. Set the first’s IsChecked property to True in the Object Inspector, then handle its OnChange event as thus:

procedure TForm1.rdoStandardChange(Sender: TObject);
begin
  if rdoStandard.IsChecked then
    GlyphsManager.GlyphsStyle := gsStandard;
end;

Similarly, handle rdoAlternative’s OnChange event like this:

procedure TForm1.rdoStandardChange(Sender: TObject);
begin
  if rdoAlternative.IsChecked then
    GlyphsManager.GlyphsStyle := gsAlternative;
end;

Finally, add App.Glyphs to the form’s implementation section uses clause (Alt+F11) and run the application. If all goes well, clicking on the radio boxes should toggle the button glyph:

GlyphStyleDemo - standard
GlyphStyleDemo - alternative

One thing to watch out for is that if you save everything, close and then reopen the project and have the form open in the IDE without either StdGlyphsForm or AltGlyphsForm too, the button’s styling will revert to a normal TPanel. This is nothing to worry about – close the form, open one of the glyph forms, then reopen the form, and the image will be back.

At the end of the day, I’m pretty happy with all this. While there was the odd irritating oversight in the FireMonkey source to work around, overall I find the style system here producing a more convenient approach that the VCL – in particular, it’s much nicer to be able to refer to a glyph by name rather than a meaningless numerical index. Moreover, the cosmetic issue of the custom style apparently being ‘lost’ when the main form is open without one of the glyph forms alongside is just that – cosmetic. In contrast, if the form were a VCL one with a TToolBar referencing a TImageList on a data module, the IDE would silently clear the actual reference!

Postscript – using TSubImage

As a final note, there is one variant of my approach that you might wish to consider. This is to replace the individual TImage instances with individual TSubImage ones, which then reference a single big TImage (TSubImage isn’t automatically registered, annoyingly enough, so you’ll need to do what Jeremy North did for TStyleTag here first). The reason is that when a control is used as a style, a copy of it is taken – and one aspect of the copy in a TImage case will be the contents of its Bitmap property.

If this were the VCL that wouldn’t be an issue due to the fact TBitmap there implements a reference counting system internally. In other words, when you do DestBitmap.Assign(SourceBitmap) in a VCL application, no ‘deep copy’ of the source bitmap is made, similar to how DelphiString2 := DelphiString1 doesn’t cause character data to be duplicated ‘there and then’ – rather, copying only happens when one of the two bitmaps or strings is modified later (the jargon for this is ‘copy on write semantics’). Unfortunately, the FMX TBitmap does not implement this behaviour though, and because of that, doing DestBitmap.Assign(SourceBitmap) causes an immediate ‘deep copy’ of the source, duplicating the bitmap bits in memory. For a small application with a small number of glyphs that won’t matter, and perhaps it won’t matter much either in a bigger program. As an application grows you might want to at least consider swapping out the individual TImage controls for TSubImage ones though. Thanks to the flexibility of the styling system, the task shouldn’t be too hard – all existing style associations can be kept intact.