Here is some kind of "advanced" solution taking advantage of the internal structure of the grid. The grid works internally with a TVirtualGrid containing pointers to TCellProps records, and these records contain elements for the cell text, the cell data as well as cell attributes :
type
PCellProps= ^TCellProps;
TCellProps=record
Attr: pointer;
Data: TObject;
Text: pchar;
end;
The Attr pointer is not used and is available to store cell background and text foreground colors:
type
TCellAttr = record
BkColor: TColor;
TxtColor: TColor;
end;
PCellAttr = ^TCellAttr;
We implement properties CellBkColor[ACol, ARow]: TColor and CellTxtColor[ACol, ARow] and for this purpose we access the internal virtual grid introduced in TCustomDrawGrid as FGrid. And this virtual grid has a property to access the pointers to the TCellProps records, named Celda[ARow, ACol]: PCellProps ("celta" = Portuguese for "cell").
This is the getter for CellBkColor (that for CellTxtColor is similar): It reads the CellProps records and the Attr record to get the field with the background color of the specified cell (when the pointer to them are nil, the default grid Color is returned)
function TStringGrid.GetCellBkColor(ACol, ARow: Integer): TColor;
var
C: PCellProps;
begin
Result := Color;
C:= FGrid.Celda[ACol, ARow];
if (C = nil) or (C^.Attr = nil) then
Result := Color
else
Result := PCellAttr(C^.Attr)^.BkColor;
end;
and the setter for it: it allocates a new CellProps and within it an Attrib record in which the new background color can be stored:
procedure TStringGrid.SetCellBkColor(ACol, ARow: Integer; AValue: TColor);
var
C: PCellProps;
begin
C := FGrid.Celda[ACol, ARow];
if C <> nil then
begin
if C^.Attr = nil then
begin
New(PCellAttr(C^.Attr));
PCellAttr(C^.Attr)^.BkColor := Color;
PCellAttr(C^.Attr)^.TxtColor := Font.Color;
end;
if AValue <> PCellAttr(C^.Attr)^.BkColor then
PCellAttr(C^.Attr)^.BkColor := AValue;
UpdateCell(ACol, ARow);
Modified := True;
end else
begin
New(C);
New(PCellAttr(C^.Attr));
PCellAttr(C^.Attr)^.BkColor := AValue;
PCellAttr(C^.Attr)^.TxtColor := Font.Color;
C^.Text := nil;
C^.Data := nil;
FGrid.Celda[ACol, ARow] := C;
UpdateCell(ACol, ARow);
Modified := True;
end;
end;
With these new properties
property CellBkColor[ACol, ARow: Integer]: TColor read GetCellBkColor write SetCellBkColor;
property CellTxtColor[ACol, ARow: Integer]: TColor read GetCellTxtColor write SetCellTxtColor;
the cell background and text colors can be defined as simply as
procedure TForm1.FormCreate(Sender: TObject);
begin
StringGrid1.CellBkColor[1, 1] := clYellow;
StringGrid1.CellTxtColor[2, 1] := clRed;
end;
But: this is not yet the whole story. We must provide a way to apply the new colors when the cell is painted, and we must make sure that the memory allocated for the TCellAttr record is disposed when the cell is destroyed:
For application of the colors in the Attr field we hook into the PrepareCanvas mechanism. PrepareCanvas is called immediately before a cell is painted. We must check whether a cell stores colors inside the virtual grid and must extract these colors and set them to the grid's canvas:
procedure TStringGrid.PrepareCanvas(ACol, ARow: Integer; AState:TGridDrawState);
var
C: PCellProps;
savedHandler: TOnPrepareCanvasEvent;
isSelected: Boolean;
begin
savedHandler := OnPrepareCanvas;
try
OnPrepareCanvas := nil;
inherited;
GetSelectedState(AState, isSelected);
C := FGrid.Celda[ACol, ARow];
if (C <> nil) and not isSelected then
begin
Canvas.Brush.Color := GetCellBkColor(ACol, ARow);
Canvas.Font.Color := GetCellTxtColor(ACol, ARow);
end;
finally
OnPrepareCanvas := savedHandler;
DoPrepareCanvas(ACol, ARow, AState);
end;
end;
Next point: The internal grid must take care of disposing the memory allocated for the Attr field. The virtual grid has a method DisposeCell called whenever a cell is destroyed, and this is where we can dispose the memory:
procedure TCellAttrVirtualGrid.DisposeCell(var P: PCellProps);
begin
if Assigned(P) and Assigned(P^.Attr) then Dispose(PCellAttr(P^.Attr));
inherited;
end;
This is an overridden virtual method and implemented in a derived class, TCellAttrVirtualGrid. Of course, the StringGrid must be told to create this modified virtual grid instead of its default one - otherwise the new DisposeCell would never be called. But luckily the grid has a method CreateGrid where the virtual grid is created, and here we can create the TCellAttrVirtualGrid instead of the default one:
function TStringGrid.CreateVirtualGrid: TVirtualGrid;
begin
Result := TCellAttrVirtualGrid.Create;
end;
Yes, that's all (well -- mostly, except for a helper which I dropped here for clarity). The important point left is: we need to subclass the TStringGrid in order to put all this in its correct place. Subclassing can be done by giving the StringGrid class a new name, e.g. TColoredStringGrid, or by descending the new StringGrid from Grids.TStringGrid in the same unit of the form where it is used (alternatively the code can also be put into a separate unit, but that must be used at the end of the "uses" line).
The entire (tested) code is in the attachment. You can easily extend it to add other attributes to the TCellAttr record (e.g. text alignment, font styles, line-break, ...)
[EDIT]
I committed an extended version of this sample project to the examples/gridexamples/attribute_grid folder of Lazarus/main. Just copy the unit "attrgrid.pas" from that folder to your project and list its name at the end of the "uses" clause, and you have the new feature that cell attributes such as font (size, name, color, style), text alignment, word-break and backgroundcolor can be accessed simply by properties:
Examples:
StringGrid1.CellBkColor[3, 3] := clYellow;
StringGrid1.CellAlignment[4, 4] := taRightJustify;
StringGrid1.CellWordWrap[4, 4] := true;
MyColor := StringGrid1.CellBkColor[3, 3];