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<T: TStyledControl> = class(TColumn)
private
FOnCellCreated: TCellCreatedEvent;
protected
function CreateCellControl: TStyledControl;override;
public
procedure DoChanged(Sender: TObject);
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>.CreateCellControl: TStyledControl;
begin
Result := TStyledControlClass(T).Create(Self);
if Assigned(OnCellCreated) then
OnCellCreated(Self, Result);
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(Sender: TObject);
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(Sender: TObject);
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(Sender: TObject; var Cell: TStyledControl);
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>.CreateCellControl: TStyledControl;
var
C: TRTTIContext;
RT: TRTTIType;
P: TRTTIProperty;
PT: TRTTIType;
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.
C := TRTTIContext.Create;
We get a TRTTIType for our class
RT := C.GetType(Result.ClassInfo);
if RT <> nil then
begin
We find the OnChange property.
P := RT.GetProperty('OnChange');
if P <> 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(Result, TValue.From<TNotifyEvent>(DoChanged));
end;
end;
end;
if Assigned(OnCellCreated) then
OnCellCreated(Self, Result);
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.
Full Source
unit GridColumns;
interface
uses FMX.Grid, FMX.Types;
type TStyledControlClass = class of TStyledControl;
type TCellCreatedEvent = procedure(Sender: TObject;var Cell:TStyledControl) of object;
type TGenericColumn<T: TStyledControl> = class(TColumn)
private
FOnCellCreated: TCellCreatedEvent;
protected
function CreateCellControl: TStyledControl;override;
public
procedure DoChanged(Sender: TObject);
published
property OnCellCreated: TCellCreatedEvent read FOnCellCreated write FOnCellCreated;
end;
implementation
uses RTTI, Classes;
{ TGenericColumn<T> }
function TGenericColumn<T>.CreateCellControl: TStyledControl;
var
C: TRTTIContext;
RT: TRTTIType;
P: TRTTIProperty;
PT: TRTTIType;
begin
Result := TStyledControlClass(T).Create(Self);
C := TRTTIContext.Create;
RT := C.GetType(Result.ClassInfo);
if RT <> nil then
begin
P := RT.GetProperty('OnChange');
if P <> nil then
begin
if P.PropertyType.QualifiedName = 'System.Classes.TNotifyEvent' then
begin
P.SetValue(Result, TValue.From<TNotifyEvent>(DoChanged));
end;
end;
end;
if Assigned(OnCellCreated) then
OnCellCreated(Self, Result);
end;
procedure TGenericColumn<T>.DoChanged(Sender: TObject);
begin
DoTextChanged(Sender);
end;
end.
Previous Comments
#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.





#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.