1. Home
  2. Computing & Technology
  3. Delphi Programming
Professional Looking Grids with Custom Drawing
Page 2: How to add clickable buttons and checkboxes inside cells.
 Win prizes by sharing code!
Do you have some Delphi code you want to share? Are you interested in winning a prize for your work?
Delphi Programming Quickies Contest
 More of this Feature
• Page 1: Fixing Column Headers
• Page 3: Merging Cells

• Download Sample Project
 Join the Discussion
"Post your views, comments, questions and doubts to this article."
Discuss!
 Related Resources
• Get Crazy - Draw Yourself
• Custom Draw Menus
• Graphical Combos
• Custom shaped forms and controls

Drawing buttons
It is possible to include components for editing inside cells, but if a checkbox or a simple button is all you need you can easily do it with some custom drawing. First of all we have to draw the button, and then we need to find out if the button was pressed.

About Delphi Programming Custom Drawing: TStringGrid

I like to include a separate routine for calculation where the button should be drawn. This routine has a boolean parameter called 'complete'. This is for getting the complete area set aside for the button. Not all of this area will be used for the actual drawing, so if you set complete to false you get the inner rectangle where the actual button should be drawn. As you can see, we have three buttons, each with different justifications.

//Returns rectangle where button will be drawn:
function TfrmMain.GetBtnRect(ACol, ARow: integer; complete: boolean): TRect;
  function MakeBtnRect(Alignment: TAlignment; cellrect: TRect; 
                  complete: boolean): TRect;
  begin
    (*
    You can find the implementation in the 
    source provided for the sampleapplication
    *)
  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
    //First row (header) has a rightaligned sort button:
    result := MakeBtnRect(taRightJustify, cellrect, complete);
  end
  else
  begin
    //Additional lines have three buttons:
    case ACol of
      0: result := MakeBtnRect(taLeftJustify, cellrect, complete);
      2: result := MakeBtnRect(taRightJustify, cellrect, complete);
      3: result := MakeBtnRect(taCenter, cellrect, complete);
    end;
  end;
end;

The next code listing shows the actual drawing for the data rows. In its basic form, it includes the same steps as for drawing the header, but there is some additional code for drawing the buttons. The first difference you will notice is the calculation of the text position. For the cells containing both buttons and text, we have to set aside some space for the button. The next addition is the code for showing if a row is selected or not. Then, after drawing the text, the fun begins.

For each cell containing a button, we calculate the space occupied by the button, erase the area, and draw the button. For the checkboxes I've used another API routine called DrawFrameControl. This routine is used to draw framed controls like buttons, scrollbars, menus and title bars. Again you could open up the documentation for a complete overview of what the routine is capable of. I will not discuss if this routine draws nice checkboxes or not, but I have choose to use DrawEdge( ), yet another API routine, for the regular button.

procedure TfrmMain.gridOwnerDrawDrawCell
  (Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
  //Make some text to display in cell:
  function GetText(ACol, ARow: integer): string;
  begin
    [...]
  else if ACol <> 3 then
      result := format('Data %2.2d:%2.2d', [ACol, ARow]);
  end;
var
  txtRect: TRect;
  btnRect: TRect;
  btnState: integer;
  tmpstr, str: string;
  tmpRect: TRect;
  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 = 0 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.Right := Rect.Right - BTN_WIDTH - TXT_MARG.x - TXT_MARG.x;
      txtRect.left := Rect.Left + TXT_MARG.x;
    end
    else
    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 text:
    str := GetText(ACol, ARow);
    gridOwnerDraw.Canvas.Font.Name := gridOwnerDraw.Font.Name;
    gridOwnerDraw.Canvas.Font.Size := gridOwnerDraw.Font.Size;
    DrawText(gridOwnerDraw.canvas.Handle, PChar(str), length(str), 
             txtRect, DT_SINGLELINE or DT_LEFT or DT_VCENTER or DT_END_ELLIPSIS);
    //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 ARow mod 2 = 1 then
        btnState := btnState or DFCS_CHECKED;
      DrawFrameControl(gridOwnerDraw.canvas.handle, btnRect, DFC_BUTTON, btnState)
    end
    else if ACol = 2 then
    begin
      //Get buttonposition and draw button:
      btnRect := GetBtnRect(ACol, ARow, false);
      gridOwnerDraw.Canvas.Brush.Color := clBtnFace;
      gridOwnerDraw.Canvas.Pen.Style := psClear;
      gridOwnerDraw.Canvas.Rectangle(btnRect);
      DrawEdge(gridOwnerDraw.canvas.Handle, btnRect, EDGE_RAISED, 
               BF_FLAT or BF_RECT or BF_ADJUST);
      gridOwnerDraw.Canvas.Font.Name := 'Arial';
      gridOwnerDraw.Canvas.Font.Size := 8;
      gridOwnerDraw.Canvas.Font.Color := clBlack;
      DrawText(gridOwnerDraw.canvas.Handle, '...', -1, btnRect, 
               DT_SINGLELINE or DT_CENTER or DT_VCENTER);
    end
    else if ACol = 3 then
    begin
      //Get buttonposition and draw checkbox:
      btnRect := GetBtnRect(ACol, ARow, false);
      gridOwnerDraw.canvas.Brush.Color := clWindow;
      gridOwnerDraw.canvas.FillRect(btnrect);
      btnState := DFCS_BUTTONCHECK or DFCS_FLAT;
      DrawFrameControl(gridOwnerDraw.canvas.handle, btnRect, DFC_BUTTON, btnState)
    end;
    //If selected, draw focusrect:
    if gdSelected in State then
    with gridOwnerDraw.canvas do begin
      Pen.Style := psInsideFrame;
      Pen.Color := clBtnShadow;
      Polyline([Point(focusRect.left-1, focusRect.Top), 
                Point(focusRect.right-1, focusRect.Top)]);
      Polyline([Point(focusRect.left-1, focusRect.Bottom-1), 
                Point(focusRect.right-1, focusRect.Bottom-1)]);
      if ACol = 0 then
        Polyline([Point(focusRect.left-1, focusRect.Top), 
                  Point(focusRect.left-1, focusRect.Bottom-1)])
      else if ACol = gridOwnerDraw.ColCount - 1 then
        Polyline([Point(focusRect.right-1, focusRect.Top), 
                  Point(focusRect.right-1, focusRect.Bottom-1)]);
    end;
  end;
end;

Now that the user can see the button, we need to handle some clicking. First we find the exact position and cell where the user clicked. The mouse coordinates needs to be translated from 'screen space' to 'grid space'. Then we can check if the point is inside the button rect. I have inflated the button Rect by two pixels so it is easier to hit the button. Notice the FInMouseClick check. In some rare occasions, the events are fired more then once. It shouldn't be a problem in this case, but I like to leave the checks there, just in case.

procedure TfrmMain.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
            0: ShowMEssage('Clicked checkbox in first column');
            2: ShowMEssage('Clicked button in third column');
            3: ShowMEssage('Clicked checkbox in last column');
          end;
        end;
      end;
    finally
      FInMouseClick := false;
    end;
  end;
end;

So now we have clickable buttons in our grid. The checkboxes will not work as expected, but it would be easy to modify the code to store the check states.

In the sample application you will see that I have included some of the same methods for drawing small arrows in the column header, something that could be useful for marking sorted columns. I have used OnMouseDown to handle clicking inside the header, since OnClick isn't fired for fixed cells.

When I need to present data grouped into subcategories, I often want the subheadings to flow across cell boundaries. One solution for this to happen is to merge all the columns into one wide cell...

Next page > How to simulate merged cells > Page 1, 2, 3

Explore Delphi Programming
About.com Special Features

Stay connected and entertained with reviews on tips on the latest HDTVs, cellphones and more. More >

Easy ways to connect two computers for networking purposes. More >

  1. Home
  2. Computing & Technology
  3. Delphi Programming

©2009 About.com, a part of The New York Times Company.

All rights reserved.