delphicheckboxstringgrid

Checkboxes in StringGrid do not toggle normally: Delphi


I've modified the Delphi code found here to save the checkbox state in a three column stringgrid. The problem is that you have to click a cell twice in order to toggle it. If I set goEditing then you can set the state with one click but one more click makes the checkbox invisible. How can I prevent these edit state problems?

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Grids;

type
  TForm1 = class(TForm)
    gridOwnerDraw: TStringGrid;//must set goEditing True
    procedure gridOwnerDrawDrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
    procedure gridOwnerDrawClick(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
    FInMouseClick: boolean;
    function GetBtnRect(ACol, ARow: integer; complete: boolean): TRect;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

const
  TXT_MARG: TPoint = (x: 4; y: 2);
  BTN_WIDTH = 12;
var
  Checked1: array[1..4] of boolean = (false, true, false, true);
  Checked2: array[1..4] of boolean = (true, false, true, false);

//Returns rectangle where button will be drawn:
procedure TForm1.FormResize(Sender: TObject);
begin
  gridOwnerDraw.Invalidate;
end;

function TForm1.GetBtnRect(ACol, ARow: integer; complete: boolean): TRect;

  function MakeBtnRect(Alignment: TAlignment; cellrect: TRect; complete: boolean): TRect;
  var
    rowHeight: integer;
  begin
    result := cellrect;
    rowheight := cellrect.bottom - cellrect.top;

    case Alignment of
      taLeftJustify:
        begin
          result.Right := cellrect.left + BTN_WIDTH + TXT_MARG.x + (TXT_MARG.x div 2);
          if not complete then
          begin
            result.Top := cellrect.Top + ((RowHeight - BTN_WIDTH) div 2);
            result.Left := cellrect.Left + ((RowHeight - BTN_WIDTH) div 2);
            result.Bottom := result.Top + BTN_WIDTH;
            result.Right := result.Left + BTN_WIDTH;
          end;
        end;
      taRightJustify:
        begin
          result.Left := cellrect.Right - BTN_WIDTH - TXT_MARG.x - TXT_MARG.x;
          if result.left < cellrect.left then
            result.left := Cellrect.left;

          if not complete then
          begin
            result.top := cellrect.top + ((RowHeight - BTN_WIDTH) div 2);
            result.left := result.left + TXT_MARG.x;
            result.right := Result.left + BTN_WIDTH;
            result.Bottom := result.top + BTN_WIDTH;
          end;
        end;
      taCenter:
        begin
          result.left := result.left + ((cellrect.Right - cellrect.left) div 2) - (BTN_WIDTH div 2) - TXT_MARG.x;
          if result.left < cellrect.Left then
            result.left := cellrect.left;
          result.right := result.left + BTN_WIDTH + TXT_MARG.x + TXT_MARG.x;
          if not complete then
          begin
            result.Top := cellrect.Top + ((RowHeight - BTN_WIDTH) div 2);
            result.Left := result.Left + TXT_MARG.x;
            result.Bottom := result.Top + BTN_WIDTH;
            result.Right := result.Left + BTN_WIDTH;
          end;
        end;
    end;
  end;

var
  cellrect: TRect;
begin
  result := Rect(0, 0, 0, 0);

  //Get complete cellrect for the current cell:
  cellrect := gridOwnerDraw.CellRect(ACol, ARow);

  //Last visible row sometimes get truncated so we need to fix that
  if (cellrect.Bottom - cellrect.Top) < gridOwnerDraw.DefaultRowHeight then
    cellrect.Bottom := cellrect.top + gridOwnerDraw.DefaultRowheight;

  if ARow > 0 then
  begin
    //Additional lines have two buttons:
    case ACol of
      1: result := MakeBtnRect(taCenter, cellrect, complete);
      2: result := MakeBtnRect(taCenter, cellrect, complete);
    end;
  end;
end;

procedure TForm1.gridOwnerDrawClick(Sender: TObject);
var
  where: TPoint;
  ACol, ARow: integer;
  btnRect: TRect;
begin
  //Again, check to avoid recursion:
  if not FInMouseClick then
  begin
    FInMouseClick := true;
    try
      //Get clicked coordinates and cell:
      where := Mouse.CursorPos;
      where := gridOwnerDraw.ScreenToClient(where);
      gridOwnerDraw.MouseToCell(where.x, where.y, ACol, ARow);
      if ARow > 0 then
      begin
        //Get buttonrect for clicked cell:
        btnRect := GetBtnRect(ACol, ARow, false);
        InflateRect(btnrect, 2, 2);  //Allow 2px 'error-range'...

        //Check if clicked inside buttonrect:
        if PtInRect(btnRect, where) then
        begin
          case ACol of
            1: Checked1[ARow]:= Not Checked1[ARow];
            2: Checked2[ARow]:= Not Checked2[ARow];
          end;
        end;
      end;
    finally
      FInMouseClick := false;
    end;
  end;
end;

procedure TForm1.gridOwnerDrawDrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  txtRect: TRect;
  btnRect: TRect;
  btnState: integer;
  focusRect: TRect;
begin
  //If header is to be drawn:
  if ARow = 0 then
  begin
  end

  //For the rest of the rows:
  else
  begin
    //Setting canvas properties and erasing old cellcontent:
    gridOwnerDraw.Canvas.Brush.Color := clWindow;
    gridOwnerDraw.Canvas.Brush.Style := bsSolid;
    gridOwnerDraw.Canvas.Pen.Style := psClear;
    gridOwnerDraw.Canvas.FillRect(rect);

    //Textposition:
    txtRect := Rect;
    focusRect := Rect;
    if ACol = 1 then
    begin
      txtRect.Left := Rect.left + BTN_WIDTH + TXT_MARG.x + TXT_MARG.x;
      focusRect.Left := txtRect.Left;
    end
    else if ACol = 2 then
    begin
      txtRect.Left := Rect.left + TXT_MARG.x;
    end;

    //Drawing selection:
    gridOwnerDraw.Canvas.Font.Style := [];
    if (gdSelected in State) then
    begin
      gridOwnerDraw.Canvas.Brush.Color := clbtnFace;
      gridOwnerDraw.Canvas.Font.Color := clBlue;
    end
    else
    begin
      gridOwnerDraw.Canvas.Brush.Color := clWindow;
      gridOwnerDraw.Canvas.Font.Color := clWindowText;
    end;
    gridOwnerDraw.canvas.FillRect(Rect);

    //Drawing buttons:
    if ACol > 0 then
    begin
      //Clear buttonarea:
      btnRect := GetBtnRect(ACol, ARow, true);
      gridOwnerDraw.canvas.Brush.Color := clWindow;
      gridOwnerDraw.canvas.FillRect(btnrect);

      //Get buttonposition and draw checkbox:
      btnRect := GetBtnRect(ACol, ARow, false);
      btnState := DFCS_BUTTONCHECK or DFCS_FLAT;
      if (ACol=1) and Checked1[ARow] then
        btnState := btnState or DFCS_CHECKED
      else if (ACol=1) then
        btnState := btnState or DFCS_BUTTONCHECK
      else if (ACol=2) and Checked2[ARow] then
        btnState := btnState or DFCS_CHECKED
      else if (ACol=2) then
        btnState := btnState or DFCS_BUTTONCHECK;
      DrawFrameControl(gridOwnerDraw.canvas.handle, btnRect, DFC_BUTTON, btnState)
    end;

    //If selected, draw focusrect:
    if gdSelected in State then
    begin
      gridOwnerDraw.canvas.pen.Style := psInsideFrame;
      gridOwnerDraw.canvas.pen.Color := clBtnShadow;
      gridOwnerDraw.canvas.Polyline([Point(focusRect.left-1, focusRect.Top), Point(focusRect.right-1, focusRect.Top)]);
      gridOwnerDraw.canvas.Polyline([Point(focusRect.left-1, focusRect.Bottom-1), Point(focusRect.right-1, focusRect.Bottom-1)]);
      if ACol = 1 then
        gridOwnerDraw.canvas.Polyline([Point(focusRect.left-1, focusRect.Top), Point(focusRect.left-1, focusRect.Bottom-1)])
      else if ACol = gridOwnerDraw.ColCount - 1 then
        gridOwnerDraw.canvas.Polyline([Point(focusRect.right-1, focusRect.Top), Point(focusRect.right-1, focusRect.Bottom-1)]);
    end;
  end;
end;

end.

Solution

  • You need two clicks because you are not triggering a paint after you determine a click is in a check box boundary. The second click invalidates the previously selected cell, whether it's the same cell or not, hence then the switched state of the check box is now reflected.

    Invalidate the check box to have it repainted:

    procedure TForm1.gridOwnerDrawClick(Sender: TObject);
    var
      where: TPoint;
      ACol, ARow: integer;
      btnRect: TRect;
    begin
          ..
            ...
            if PtInRect(btnRect, where) then
            begin
              case ACol of
                1: Checked1[ARow]:= Not Checked1[ARow];
                2: Checked2[ARow]:= Not Checked2[ARow];
              end;
              InvalidateRect(gridOwnerDraw.Handle, @btnRect, True);  // <-Here
            end;
          end;
        finally
          FInMouseClick := false;
        end;
      end;
    end;
    

    Because of the invalidation, your gridOwnerDrawDrawCell will be called drawing the appropriate check state.