A FireMonkey Grid Column for any Control

One of the great things about a FireMonkey grid is that each column can contain any control class. The downside is that to do so you need to go to a certain amount of work.

Wouldn’t it be nice if there was an easy was to create a column for any class of control? Imagine doing this with generics so you could simply say

Grid1.AddObject(TGenericColumn<TCalendarEdit>.Create(Grid1); 

If so, read on, you’re in for a fun ride through FireMonkey, generics and RTTI.

TGenericColumn

Here’s our class definition. Really simple.

type TGenericColumn<TTStyledControl> = class(TColumn)
  private
    
FOnCellCreatedTCellCreatedEvent;
  protected
    function 
CreateCellControlTStyledControl;override;
  public
    
procedure DoChanged(SenderTObject);
  
published
    property OnCellCreated
TCellCreatedEvent read FOnCellCreated write FOnCellCreated;
  
end

We simply subclass TColumn (which by default gives us a column of TEdits. Sadly there’s not an abstract TGenericColumn class available, which would have been a more appropriate better parent).

In our type declaration we specify

so the compiler will validate that we get a descendant of TStyledControl when the class is instantiated. Since a grid cell can be any descendant of TStyledControl this verifies we will be using an appropriate component class.

We override CreateCellControl to do the work of creating the control,

function TGenericColumn<T>.CreateCellControlTStyledControl;
begin
  Result 
:= TStyledControlClass(T).Create(Self);
  if 
Assigned(OnCellCreatedthen
    OnCellCreated
(SelfResult);
end

CreateCellControl needs to do two things. First it needs to create the control, then it needs to set handlers for any OnChange events of the control so that it can let the grid know when the controls data value changes. The grid will then fire it’s OnSetValue event so the app can monitor the state of the grid. CreateCellControl can also set any properties of the cell which are needed.

The first line creates the control. We can’t directly call create on T, instead we need to get the class for it. We cast T to TStyledControlClass do do this, which means we need to declare TStyledControlClass in the interface of the unit,

type TStyledControlClass = class of TStyledControl

Assigning event handlers is a difficult one. We don’t know which class is being created, so we don’t know what it’s OnChange event handler(s) may be called. And there’s no common ancestor with an OnChange property.

Instead I created an OnCellCreated event. This is called after creation and enables us to add event handlers and set properties as necessary.

Which brings us to the DoChanged event handler. This is where any change events need to be pointed to and it simply calls the DoTextChanged method of TColumn which in turn handles updating the grid.

procedure TGenericColumn<T>.DoChanged(SenderTObject);
begin
  DoTextChanged
(Sender);
end

Using the Column

So now we can start using the column. We need to create the column(s),

procedure TForm1.FormCreate(SenderTObject);
begin
  Data 
:= TList<TList<TValue>>.Create;
  
DefaultData := TList<TValue>.Create;
  
Grid1.AddObject(TGenericColumn<TCalendarEdit>.Create(Self));
  
TGenericColumn<TCalendarEdit>(Grid1.Columns[0]).OnCellCreated := EVCellCreated;

  
Grid1.AddObject(TGenericColumn<THueTrackBar>.Create(Self));
  
TGenericColumn<THueTrackBar>(Grid1.Columns[1]).OnCellCreated := EVCellCreated;
end

And handle the OnCellCreated events,

procedure TForm1.EVCellCreated(SenderTObject; var CellTStyledControl);
begin
  
if Cell is TCalendarEdit then
    TCalendarEdit
(Cell).OnChange := TGenericColumn<TCalendarEdit>(Grid1.Columns[0]).DoChanged;

  if 
Cell is THueTrackBar then
    THueTrackBar
(Cell).OnChange := TGenericColumn<THueTrackBar>(Grid1.Columns[1]).DoChanged;
end

It all works nicely, but that code is not easy to read. And all it’s doing in these examples is setting the OnChange event handler of the cell control. Let’s have another look at setting the event automatically.

Bring on RTTI

Whilst we can’t guarantee it, most controls simply have an event named OnChange of type TNotifyEvent which is the only event we need to plug into. So, all we need is to get CreateCellControl to look inside the control for an OnChange event and point it to DoChanged. And this is exactly the type of thing which RTTI was invented for.

Here’s our new CreateCellControl,

function TGenericColumn<T>.CreateCellControlTStyledControl;
var
  
CTRTTIContext;
  
RTTRTTIType;
  
PTRTTIProperty;
  
PTTRTTIType;
begin
  Result 
:= TStyledControlClass(T).Create(Self); 

A TRTTIContext gives us access to RTTI features. It’s a record, so there’s no need to free it.

:= TRTTIContext.Create

We get a TRTTIType for our class

RT := C.GetType(Result.ClassInfo);
  if 
RT <> nil then
  begin 


We find the OnChange property.

:= RT.GetProperty('OnChange');
    if 
<> nil then
    begin 


We validate the property is of the correct type.

if P.PropertyType.QualifiedName 'System.Classes.TNotifyEvent' then
      begin 


And finally we set the property value for our instance.

P.SetValue(ResultTValue.From<TNotifyEvent>(DoChanged));
      
end;
    
end;
  
end;

  if 
Assigned(OnCellCreatedthen
    OnCellCreated
(SelfResult);
end

58 Lines

‘Simples’ as they say on an advert over here. And the whole thing was achieved in only 58 lines of code!

Now it does have the disadvantage that you need to create the columns in code, rather than in the form editor. It would theoretically be possibly to have a string property to specify the class and create the cells from that (another bit of fun advanced Delphi coding) but I could forsee problems with creation order. I.e. The column would be created before the CellClass property was set (or changed) and there would need to be a way to change all the cells already created. I’ll leave that as a challenge for another day.

And if you’re interested the rest of the code in the sample app is there to store and retrieve the cell values for the grid’s OnSetValue and OnGetValue events. Which brings to mind an idea to create a grid class which could take any column class and store it’s own values in the same way that a TStringGrid does with strings. Another project for a rainy winters day.

Download the full source:

Full Source

unit GridColumns;

interface
uses FMX.GridFMX.Types;

type TStyledControlClass = class of TStyledControl;

type TCellCreatedEvent procedure(SenderTObject;var Cell:TStyledControlof object;

type TGenericColumn<TTStyledControl> = class(TColumn)
  private
    
FOnCellCreatedTCellCreatedEvent;
  protected
    function 
CreateCellControlTStyledControl;override;
  public
    
procedure DoChanged(SenderTObject);
  
published
    property OnCellCreated
TCellCreatedEvent read FOnCellCreated write FOnCellCreated;
  
end;

implementation
uses RTTI
Classes;

{ TGenericColumn<T}

function TGenericColumn<T>.CreateCellControlTStyledControl;
var
  
CTRTTIContext;
  
RTTRTTIType;
  
PTRTTIProperty;
  
PTTRTTIType;
begin
  Result 
:= TStyledControlClass(T).Create(Self);

  
:= TRTTIContext.Create;
  
RT := C.GetType(Result.ClassInfo);
  if 
RT <> nil then
  begin
    P 
:= RT.GetProperty('OnChange');
    if 
<> nil then
    begin
      
if P.PropertyType.QualifiedName 'System.Classes.TNotifyEvent' then
      begin
        P
.SetValue(ResultTValue.From<TNotifyEvent>(DoChanged));
      
end;
    
end;
  
end;

  if 
Assigned(OnCellCreatedthen
    OnCellCreated
(SelfResult);
end;

procedure TGenericColumn<T>.DoChanged(SenderTObject);
begin
  DoTextChanged
(Sender);
end;

end

Previous Comments

#1 from .(JavaScript must be enabled to view this email address) on February 18, 2013

Am I reading this correctly that each and every cell has a control created for it?  (regardless of whether it is displayed or not)

For a large grid, that could lead to a LOT of controls being created.

#2 from .(JavaScript must be enabled to view this email address) on February 18, 2013

No, FireMonkey only creates enough controls as it needs to the current view and creates/frees if the grid is resized. Which is why the sample project has to cache the data from the grid as controls are edited.

#3 from .(JavaScript must be enabled to view this email address) on March 27, 2013

Heck of an article.  I’d bookmark the Blogs just for the cool code examples.

I would also like to buy the styler (have already downloaded it), but I am concerned about the continuous updates policy.  The web site here indicates an update each month between Oct and Dec (build 12).  It’s now March and I see no new updates.

Hope all is well.  I am no stranger to the foibles of life getting in the way of fun.

#4 from .(JavaScript must be enabled to view this email address) on March 28, 2013

James, I’m currently taking a break from MonkeyStyler while I wait to see if it gains some traction and working crazy hours on other projects.

Commenting is not available in this channel entry.