0
votes

I am trying to make an app where the cells of a TStringGrid will change color when I click on them. Each time I click on a cell, it should switch over to the next color and stay that color, until I click on that cell again, in order:

white ==> red ==> orange ==> green ==> white (like a traffic light).

The error(s) I am getting is a bit hard to explain, but I'll try.

The application runs, but when I click on one cell and then on another, sometimes the first cell I clicked on changes color, but the second one doesn't. Other times, both cells change color. Other times, both cells just reset to their white state.

type
  TForm1 = class(TForm)
    StringGrid: TStringGrid;
    procedure StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
  private
    arrState: array[1..4, 1..4] of Integer;
end;

procedure TForm1.StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  iRow, iCol: Integer;
  arrk: array[1..4, 1..4] of Integer;
begin
  for iCol := 4 downto 1 do
  begin
    for iRow := 4 downto 1 do
    begin
      if (gdSelected in State) then
      begin
        case arrState[ARow, aCol] of
           0: begin
             StringGrid.Canvas.Brush.Color := clWhite;
             Rect := StringGrid.CellRect(iCol, iRow);
             StringGrid.Canvas.FillRect(Rect);
             Inc(arrState[iRow, iCol]);
           end;

           1: begin
             StringGrid.Canvas.Brush.Color := clRed;
             Rect := StringGrid.CellRect(iCol, iRow);
             StringGrid.Canvas.FillRect(Rect);
             Inc(arrState[iRow, iCol]);
           end;

           2: begin
             StringGrid.Canvas.Brush.Color := $008CFF;
             Rect := StringGrid.CellRect(iCol, iRow);
             StringGrid.Canvas.FillRect(Rect);
             Inc(arrState[iRow, iCol]);
           end;

           3: begin
             StringGrid.Canvas.Brush.Color := clGreen;
             Rect := StringGrid.CellRect(iCol, iRow);
             StringGrid.Canvas.FillRect(Rect);
             arrState[iRow, iCol] := 0;
           end;

         end;
       end;
     end;
   end;
 end;
1

1 Answers

3
votes

The problem is that you are using the OnDrawCell event to update your state machine. NEVER use a drawing event to drive state changes! A UI control gets painted often and for many reasons, so any drawing events should be painting only the current state, for the specific item that is currently being drawn. You should be using the OnSelectCell or OnClick event to update your state machine and then trigger a repaint of the updated cell.

Try this:

type
  TForm1 = class(TForm)
    StringGrid: TStringGrid;
    procedure StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
    procedure StringGridSelectCell(Sender: TObject; ACol, ARow: Integer;
      var CanSelect: Boolean);
  private
    arrState: array[1..4, 1..4] of Integer;
end;

procedure TForm1.StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
const
  clOrange = TColor($008CFF);
  CellColors: array[0..3] of TColor = (clWhite, clRed, clOrange, clGreen);
begin
  if (ACol in [1..4]) and (ARow in [1..4]) then
  begin
    StringGrid.Canvas.Brush.Color := CellColors[arrState[ARow, ACol]];
    StringGrid.Canvas.FillRect(Rect);
  end;
end;

// TStringGrid.InvalidateCell() is protected,
// but can be reached using an accessor class..
type
  TStringGridAccess = class(TStringGrid)
  end;

procedure TForm1.StringGridSelectCell(Sender: TObject; ACol, ARow: Integer;
  var CanSelect: Boolean);
begin
  if (ACol in [1..4]) and (ARow in [1..4]) then
  begin
    arrState[ARow, ACol] := (arrState[ARow, ACol] + 1) mod 4;
    TStringGridAccess(StringGrid).InvalidateCell(ACol, ARow);
  end;
end;

Alternatively:

type
  TForm1 = class(TForm)
    StringGrid: TStringGrid;
    procedure StringGridClick(Sender: TObject);
    procedure StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
  private
    arrState: array[1..4, 1..4] of Integer;
  end;

// TStringGrid.InvalidateCell() is protected,
// but can be reached using an accessor class..
type
  TStringGridAccess = class(TStringGrid)
  end;

procedure TForm1.StringGridClick(Sender: TObject);
type
  POINTS = packed record
    x: SHORT;
    y: SHORT;
  end;
var
  dwPos: DWORD;
  pts: POINTS absolute dwPos;
  pt: TPoint;
  iCol, iRow: Integer;
begin
  dwPos := GetMessagePos();
  pt := StringGrid.ScreenToClient(Point(pts.x, pts.y));
  StringGrid.MouseToCell(pt.X, pt.Y, iCol, iRow);
  if (iCol in [1..4]) and (iRow in [1..4]) then
  begin
    arrState[iRow, iCol] := (arrState[iRow, iCol] + 1) mod 4;
    TStringGridAccess(StringGrid).InvalidateCell(iCol, iRow);
  end;
end;

procedure TForm1.StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
const
  clOrange = TColor($008CFF);
  CellColors: array[0..3] of TColor = (clWhite, clRed, clOrange, clGreen);
begin
  if (ACol in [1..4]) and (ARow in [1..4]) then
  begin
    StringGrid.Canvas.Brush.Color := CellColors[arrState[ARow, ACol]];
    StringGrid.Canvas.FillRect(Rect);
  end;
end;