FireMonkey Grids: Basics, Custom Cells and Columns

MonkeyStyler uses a couple of heavily customised grid columns. I spent some of last week finishing off the styling for them. The whole process has taught me a lot about FireMonkey grids. Prompted by this question on StackOverflow I though now would be a good time to share some of what I have learnt.

So, in this post I’m going to cover the following:

  • How FireMonkey grids work;
  • How to create a custom cell class;
  • How to create a custom column class;

Our StackOverflow poster wanted to know how to set the alignment and color of a grid cell, so I’ll concentrate on those issues in this post. We’ll create an app which has a grid showing two columns. The first will display the row index, the second financial figures, which are right aligned and with a background which changes to red when the figure is negative. I’ll cover most of that today with the styling issues in a future post.

FireMonkey Grids

Grids in FireMonkey are much more flexible than those in the VCL. But, of course with flexibility comes complexity. Fortunately, once you’ve learnt a few basics the rest comes fairly easy.

A FireMonkey TGrid is simply a container, adapted to contain children of type TColumn. A column is simply a container for a series of cells. We’ll get to the cells later.

To start with we’ll create an app which contains a TGrid. In the Delphi form designer, right click on the empty grid and select the Element Editor. Here, add a TColumn and back out. This will be a basic column in which we’ll display the row index.

On to the code, here’s the interface:

type
  TForm1 
= class(TForm)
    
Grid1TGrid;
    
Column1TColumn;
    
procedure FormCreate(SenderTObject);
    
procedure FormDestroy(SenderTObject);
  private
  protected
    
ValueColumnTColumn;
    
DataTList<Single>;
    
procedure PopulateData;
  public
  
end


Data is a list which we populate with random values in PopulateData.

FormCreate sets up our Data values, and also creates and adds another TColumn to the grid. We’ll create this in code so it’s easier to play around with. Note that, as always in FireMonkey, we simply assign it’s Parent property to the grid to assign it to the appropriate container.

procedure TForm1.FormCreate(SenderTObject);
begin
  Data 
:= TList<Single>.Create;

  
PopulateData;
  
Grid1.RowCount := RowCount;

  
ValueColumn := TColumn.Create(Grid1);
  
ValueColumn.Parent := Grid1;
end

Go back to the form editor and create a method for the grids OnGetValue event:

procedure TForm1.Grid1GetValue(SenderTObject; const ColRowInteger;
  var 
ValueVariant);
begin
  
if Col 0 then
    Value 
:= Row
  
else if Col 1 then
    Value 
:= Data[Row];
end

What happens here? The TColumn has a number of cells to display data but it only creates as many cells as it has visible rows. I.e. if a grid has a RowCount of 100, but only 20 cells are visible then only 20 cells are created. Whenever you scroll the grid each cell gets recycled. Because of this a grid cell cannot store any data between refreshes. (Or, more accurately, it should not store data because if it does it will be displaying the wrong data).

So, every time a cell is displayed the grid calls the OnGetValue event for that cell. OnGetValue passes the column and row indexes for the cell (the virtual column and row indexes) and our event handler needs to return the value to display in the Value variant parameter.

Run the above and you’ll see something like:

(Aside: the grid appears to have a few display issues if you don’t show headers, hence why I’ve kept the headers here).

Custom Columns

Ignoring the rounding issues (we’re using Singles for simplicity here), you can see that the column of supposedly financial figures really needs to be right aligned (we’ll come to rounding and formating later).

We’re using a TColumn for the figures, which is a container for TTextCell. The definition for TTextCell is:

TTextCell = class(TEdit)
  
end


So, it’s just an edit control with the parent set to the column. Haha. Easy. To get our control right aligned we just need to set the TEdit’s TextAlign property, which we can assign when the cell is created.

Each cell is created in the columns CreateCellControl virtual method. Look for the method in TColumn and we see:

function TColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TTextCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end

So, we’ll create our own custom column and override the CreateCellControl to do our work for us:

type TFinancialColumn = class(TColumn)
  protected
    function 
CreateCellControlTStyledControl;override;
  
end;
    
    ...
    
function 
TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= inherited;
  
TEdit(Result).TextAlign := TTextAlign.taTrailing;
end

Update the FormCreate to create TFinancialColumn and we see:

Custom Cells

But we’ve now taken things as far as they can go with the built in TTextCell. It’s time to create our own cell class so we can really get things looking how we want.

Look again at the definition of TColumn.CreateCellControl. Note that it returns an object descending from TStyledControl. A TStyledControl is the parent of any control which can have styling applied. In other words, any visual control in FireMonkey. FireMonkey already does that with TCheckCell, TProgressCell and TImageCell for check boxes, progress bars and images.

And you can extend this to create custom cells using any control you like. You could go really freaky and use a list box or tree view within a cell. You probably wouldn’t want to, but FireMonkey is flexible enough to do it if you want. And all FireMonkey controls can own - be containers for - other controls. So you can create a cell which is made up of multiple controls.

Take a look at the property editor grid for MonkeyStyler in the screenshot below from the the preview images blog post and you’ll see that the values column contains:

  • A TColorBox for color previews (the Fill.Color property);
  • A TEdit (editable properties);
  • A TCheckBox (boolean properties);
  • A TButton (for the animation menu button and image)

with code to set each item’s Visible property depending on the data type.

TFinancialCell

But, enough showing off, lets get back to our financial column. The TTextCell is just a TEdit. Not much customisation we can do there. What we’ll do is create our cell as a TStyledControl which will contain an alClient aligned TEdit.

type TFinancialCell = class(TStyledControl)
  protected
    
FEditTEdit;
    
procedure SetData(const ValueVariant); override;
  public
    
constructor Create(AOwnerTComponent); override;
  
end;

...

constructor TFinancialCell.Create(AOwnerTComponent);
begin
  inherited
;
  
FEdit := TEdit.Create(Self);
  
FEdit.Parent := Self;
  
FEdit.Align := TAlignLayout.alClient;
  
FEdit.TextAlign := TTextAlign.taTrailing;
end

Now, look at the SetData method, which is the Setter for the Data property. Data is defined in TFMXObject, the parent of all FireMonkey objects. Earlier we saw how the grid fetches data for each cell by called the OnGetValue event, well after it’s fetched the data it passes it on to the cell objects Data property. So, we override the SetData method to get the data we need to display.

procedure TFinancialCell.SetData(const ValueVariant);
var 
FSingle;
begin
  inherited
;
  
:= Value;
  
FEdit.Data := Format('%m'[F]);
end

Before we can run things we need to update the columns CreateCellControl to reflect our new cell class:

function TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TFinancialCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end

Now things are looking much better:

Round up

So, we have a grid column which is formatting the data how we want. This article is already long enough, so I won’t bore you any longer, other than to say that I will return at some future point to show you how you can use styles to format the grid cells we created today.

Enjoy.

GridBasics.zip - Download the project sources.

Full source:

unit Main;

interface

uses
  System
.SysUtilsSystem.TypesSystem.UITypesSystem.ClassesSystem.Variants,
  
FMX.TypesFMX.ControlsFMX.FormsFMX.DialogsGenerics.Collections,
  
FMX.GridFMX.LayoutsFMX.EditFMX.ListBox;

type TFinancialCell = class(TStyledControl)
  protected
    
FEditTEdit;
    
procedure SetData(const ValueVariant); override;
  public
    
constructor Create(AOwnerTComponent); override;
  
end;

type TFinancialColumn = class(TColumn)
  protected
    function 
CreateCellControlTStyledControl;override;
  
end;

type
  TForm1 
= class(TForm)
    
Grid1TGrid;
    
Column1TColumn;
    
procedure FormCreate(SenderTObject);
    
procedure FormDestroy(SenderTObject);
    
procedure Grid1GetValue(SenderTObject; const ColRowInteger;
      var 
ValueVariant);
  private
  protected
    
ValueColumnTFinancialColumn;
    
DataTList<Single>;
    
procedure PopulateData;
  public
  
end;

var
  
Form1TForm1;

implementation

{$R 
*.fmx}

const RowCount 20;
{ TForm1 }

procedure TForm1
.FormCreate(SenderTObject);
begin
  Data 
:= TList<Single>.Create;

  
PopulateData;
  
Grid1.RowCount := RowCount;

  
ValueColumn := TFinancialColumn.Create(Grid1);
  
ValueColumn.Parent := Grid1;
end;

procedure TForm1.FormDestroy(SenderTObject);
begin
  Data
.Free;
end;

procedure TForm1.Grid1GetValue(SenderTObject; const ColRowInteger;
  var 
ValueVariant);
begin
  
if Col 0 then
    Value 
:= Row
  
else if Col 1 then
    Value 
:= Data[Row];
end;

procedure TForm1.PopulateData;
var 
IInteger;
begin
  
for := 1 to RowCount do
    
Data.Add((Random(10000)-1000)/100);
end;

{ TFinancialColumn }

function TFinancialColumn.CreateCellControlTStyledControl;
begin
  Result 
:= TFinancialCell.Create(Self);
  
TTextCell(Result).OnTyping := DoTextChanged;
  
TTextCell(Result).OnExit := DoTextExit;
end;

{ TFinancialCell }

constructor TFinancialCell
.Create(AOwnerTComponent);
begin
  inherited
;
  
FEdit := TEdit.Create(Self);
  
FEdit.Parent := Self;
  
FEdit.Align := TAlignLayout.alClient;
  
FEdit.TextAlign := TTextAlign.taTrailing;
end;

procedure TFinancialCell.SetData(const ValueVariant);
var 
FSingle;
begin
  inherited
;
  
:= Value;
  
FEdit.Data := Format('%m'[F]);
end;

end

Previous Comments

#1 from .(JavaScript must be enabled to view this email address) on May 29, 2012

Maybe it is out of scope of this blog post, but I am very curious about how to have different cell controls for different rows in the same column.

#2 from .(JavaScript must be enabled to view this email address) on May 31, 2012

Soren, I don’t know if you’re the same guy who asked the question on StackOverflow, but either way, look at my answer there http://stackoverflow.com/questions/10804977/firemonkey-grid-with-different-controls-in-same-column for details.

#3 from .(JavaScript must be enabled to view this email address) on June 01, 2012

Mike, I am the same guy wink and my company has decided that we will skip FireMonkey for our current project. The schedule is too tight, and I think the learning curve with FireMonkey is to steep at the moment, with the very limited documentation and samples available. The intentions with FM are truly interesting, but I’m not sure it’s quite mature yet. Thanks for your help though.
/Søren

#4 from .(JavaScript must be enabled to view this email address) on June 21, 2012

I tried this method using a TCheckBox instead TEdit, it works fine, but when I close the window, appears this.

Pointer being freed was not allocated.

I’m adding this Check column to StringGrid connected with DBLinkStringGrid and DataSet.

This sample works fine with Grid without DataSet.

#5 from .(JavaScript must be enabled to view this email address) on June 21, 2012

I tried to use your sample code of your blog in this link http://monkeystyler.com/blog/entry/firemonkey-grid-basics-custom-cells-and-columns
but I am using a Grid With DataSet, and change the TEdit for TCheckBox column, works fine, the column appears, but when I close the
application, appears this message “pointer being freed was not allocated”, When I remove the column, I don’t have problems,
then I’m sure this problems appears only when I add the column in runtime to the Grid, The grid is connected to DBLinkStringGrid with a DataSet.

#6 from .(JavaScript must be enabled to view this email address) on June 23, 2012

@Arnulfo, see my email reply - sadly I have no experience with data aware controls.

#7 from Roman Yankovsky on July 11, 2012

Mike,

If there is a way to make this new type of column to appear in IDE’s Items Editor?

#8 from .(JavaScript must be enabled to view this email address) on July 19, 2012

@Roman,
I and others have investigated and I can only assume that the types of columns available in the editor are hard coded within the IDE.

#9 from .(JavaScript must be enabled to view this email address) on July 20, 2012

Mike,

  I found the way to make a Grid with Multiselect Rows in Firemonkey, using a new calculated field with virtual column in my DataSet, I did this on Freepascal under Mac OS with MySQl Datasets, but with Firemonkey I made some changes and it’s works.

Thank you for your messages.

#10 from .(JavaScript must be enabled to view this email address) on August 13, 2012

And delete/clear row?

Function or procedure please

#11 from .(JavaScript must be enabled to view this email address) on August 13, 2012

You will need to update your back end data, then call TGrid.Repaint.

#12 from .(JavaScript must be enabled to view this email address) on August 14, 2012

@Roman, The column types should be registered to the IDE as you would register a FMX component by RegisterFmxClasses (see FMX.grid.pas how it is done).

#13 from .(JavaScript must be enabled to view this email address) on September 18, 2012

Some thoughts, as I’m going through this in XE3:

TList<generic> doesn’t work in XE3 unless you include Generics.Collections.

You can’t use TEdit without including FMX.Edit. I’m guessing they factored that out of controls or something in the latest.

The definition of SetData has changed from using a variant to using a TValue:

procedure TFinancialCell.SetData(const Value: TValue); override;

This is a “lightweight” version of the Variant type, according to the docs. You also have to change the straight variant-to-single line in SetData to:

F := Value.AsType<Single>;

Other things that might be helpful:

Noting that you typed the code in the protected section, the IDE didn’t create it.

Putting in the PopulateData routine up front.

Observations:

Display issues seem to be gone when you remove headers.

——————

Now, at this point, I have a version of your code that compiles but it gives me an exception when I try to run it. The culprit seems to be the “inherited” line of the TFinancialCell, on the second calling. Drilling down, the error occurs when Delphi tries to insert the cell into the TFinanicalCell instant.

#14 from .(JavaScript must be enabled to view this email address) on September 18, 2012

OK, I found the problem: You had two hard typecasts:

TTextCell(Result).OnTyping := DoTextChanged;
TTextCell(Result).OnExit := DoTextExit;

But it appears that in XE3 the component no longer descends from TTextCell. I changed to:
function TFinancialColumn.CreateCellControl: TStyledControl;
begin
Result := TFinancialCell.Create(Self);
// Result.OnTyping := DoTextChanged;
Result.OnExit := DoTextExit;
end;

I had to eliminate the OnTyping event as it appears to be gone.

P.S. You have the only CAPTCHA I’ve ever encountered that I can actually get right on the first try…

#15 from .(JavaScript must be enabled to view this email address) on September 18, 2012

@Blake, did you download or cut/paste the sources? The TValue and OnTyping items I can understand, but TFinancialCell makes no reference to TTextCell, and the units you mention are already ‘used’.

I’ll play with XE3 more once I’ve got MonkeyStyler out the door.

Mike

#16 from .(JavaScript must be enabled to view this email address) on September 18, 2012

I typed in and had the source at hand.

TFinancialeCell makes no reference, but TFinancialColumn does! This is from the above code:

function TFinancialColumn.CreateCellControl: TStyledControl;
begin
  Result := TFinancialCell.Create(Self);
  TTextCell(Result).OnTyping := DoTextChanged;
  TTextCell(Result).OnExit := DoTextExit;
end;

And this is from line 83 of Main.pas:


function TFinancialColumn.CreateCellControl: TStyledControl;
begin
  Result := TFinancialCell.Create(Self);
  TTextCell(Result).OnTyping := DoTextChanged;
  TTextCell(Result).OnExit := DoTextExit;
end;

#17 from .(JavaScript must be enabled to view this email address) on September 18, 2012

Hmm, it seems there’s been some muddling with CreateCellControl definitions in the article.

BTW these days I’d much rather descend the TFinancialCell from TEdit (and remove the Edit component), then change the cast to TFinancialCell. Doing that means that using the keyboard to navigate around the grid works properly and, IIRC, sorts out some issues with focusing the control/cell.

#18 from .(JavaScript must be enabled to view this email address) on October 15, 2012

CreateCellControls is not really the problem.
But you have to route the events from the inner TEdit of TFinancialCell to the outside world :o)

type
  TFinancialCell = class( TStyledControl )
  private
  function GetOnChange : TNotifyEvent;
  function GetOnTyping : TNotifyEvent;
  procedure SetOnChange( const Value : TNotifyEvent );
  procedure SetOnTyping( const Value : TNotifyEvent );
  protected
  FEdit : TEdit;
  procedure SetData( const Value : Variant ); override;
  public
  constructor Create( AOwner : TComponent ); override;
  property OnTyping : TNotifyEvent read GetOnTyping write SetOnTyping;
  property OnChange : TNotifyEvent read GetOnChange write SetOnChange;
  end;

{ TFinancialColumn }

function TFinancialColumn.CreateCellControl : TStyledControl;
begin
  Result := TFinancialCell.Create( Self );

  TFinancialCell( Result ).OnTyping := DoTextChanged;
  TFinancialCell( Result ).OnChange := DoTextChanged;
  TFinancialCell( Result ).OnExit   := DoTextExit;
end;

{ TFinancialCell }

constructor TFinancialCell.Create( AOwner : TComponent );
begin
  inherited;
  FEdit       := TEdit.Create( Self );
  FEdit.Parent   := Self;
  FEdit.Align   := TAlignLayout.alClient;
  FEdit.TextAlign := TTextAlign.taTrailing;
end;

function TFinancialCell.GetOnChange : TNotifyEvent;
begin
  Result := FEdit.OnChange;
end;

function TFinancialCell.GetOnTyping : TNotifyEvent;
begin
  Result := FEdit.OnTyping;
end;

procedure TFinancialCell.SetData( const Value : Variant );
var
  F : Single;
begin
  inherited;
  F       := Value;
  FEdit.Data := Format( ‘%m’, [F] );
end;

procedure TFinancialCell.SetOnChange( const Value : TNotifyEvent );
begin
  FEdit.OnChange := Value;
end;

procedure TFinancialCell.SetOnTyping( const Value : TNotifyEvent );
begin
  FEdit.OnTyping := Value;
end;

#19 from .(JavaScript must be enabled to view this email address) on October 16, 2012

ACtually, if I was to rewrite the article today, I’d create the cell as a descendant of TEdit:

class TFinancialCell = class(TEdit)
...

It saves so much hassle. The only time you want to create separate controls is if you need multiple controls in one cell.

#20 from .(JavaScript must be enabled to view this email address) on April 02, 2013

Hi,

is there any chance to get a c++ builder of your GridBasics.zip project?

Cheers

Dodo

#21 from .(JavaScript must be enabled to view this email address) on April 02, 2013

No, I don’t do C++. You’re free to translate yourself though. The concepts are the same, it’s just the syntax which is different.

#22 from .(JavaScript must be enabled to view this email address) on April 02, 2013

ok,

I’ll give it a try!

One more question: if at design time a prepare a grid with just two cols: string col and checkbox col, why when I check the checkbox field on a line I get all the others disabled?

Thanx in advance

Cheers

Dodo

#23 from .(JavaScript must be enabled to view this email address) on April 08, 2013

Hi,

what about adding a callback on the column header click?

More, is it possible to add an image onto the column header?

Bye

Kin

#24 from .(JavaScript must be enabled to view this email address) on April 22, 2013

This seems to lose some of the styling of a regular column. If you use the jet style - the font color seems to be lost…not sure why…there may be other style issues - not sure. My cell inherited from TTextCell. Any idea why this would happen?

#25 from .(JavaScript must be enabled to view this email address) on April 22, 2013

It sounds like there isn’t a TextCellStyle element in that style and it’s not finding the inherited EditStyle. Try explicitly setting StyleLookup for the cells to ‘EditStyle’.

#26 from .(JavaScript must be enabled to view this email address) on April 22, 2013

Editstyle messes it up quite a bit…looking in the style - there is a TextCellStyle. I even forced that style on the new column - it seems to style like the other columns - except the color of the font. I’m no style expert - I am not sure how to find the style element that is defining the font color - because when I look at the TextCellStyle - the font color should be black - but all the other cells are white…I think its losing some inheritance somewhere…Any ideas?

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

Hi Mike,

Reading your blog has been most helpful, thanks very much. I have recently upgraded to XE4 and started using FM, but struggling to get grid cells to display independently.

I have worked through the above example and then updated according to your code on http://stackoverflow.com/questions/9260355/firemonkey-grid-control-styling-a-cell-based-on-a-value-via-the-ongetvalue-fu#new-answer

To recap one of your updated procedures:

Procedure TFinancialCell.ApplyStyling;
begin
//  If IsNegative then
//  FontFill.Color:=claRed
//  else
//  FontFill.Color:=claBlack;

  Font.Style:=[TFontStyle.fsItalic];

  If IsImportant then
  Font.Style:=[TFontStyle.fsBold]
  else
  Font.Style:=[];
 
  If Assigned(Font.OnChanged) then
  Font.OnChanged(Font);
 
  Repaint;
end;

When I run the program I have two problems:

1. XE4 does not allow the FontFill code (blanked out above), so for now I can’t test the colour changes
2. The fonts do not change to italic or bold at all, I expected to at least get this to work!

Any ideas or pointers? I’m not sure if it’s a change to XE4 that’s getting in the way, I had to make a few changes to your code to get it to compile (as noted above in a previous comment). One of your other blog posts mentioned FreeStyle, but I wasn’t sure if I understood this correctly or even if it was relevant. As you can tell, I’m stumbling a bit in the dark here, any help would be appreciated.

Regards

Alex

2.

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

Any ideas on how to centre stringcolumn header text?

#29 from .(JavaScript must be enabled to view this email address) on October 22, 2013

Hi Mike

Can you help me? with how I can change the row background color in TGrid?

#30 from .(JavaScript must be enabled to view this email address) on December 17, 2013

I could not get this to work using XE5 and Livebindings to a dataset. The Grid GetValue event does not seem to get called during painting.  I could not get the sub-classed TextCell to work as it gets overwritten by the Bindings Manager.
Readers may find my solution helpful
http://stackoverflow.com/questions/19629898/firemonkey-mobile-grid-with-livebindings-changing-textcell-text-color-at-runti/19784407#19784407

Commenting is not available in this channel entry.