|
|
 |
 |
|
|
 |
 |
|
Join the Discussion
|
"Post your views, comments, questions and doubts to this article."
Discuss!
|
|
 |
 |
|
|
 |
 |
 |
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.
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
|