TStringGrid is a good component. However, it lacks a direct property or method to handle one particularly useful capability: graphics. If you've used the TStringGrid component, you know that it works well for text (hence the string in TStringGrid). But what if you want to throw a bit of graphics into a cell--a custom button or indicator light, for instance? If one column in your grid represents a Boolean (on/off) value, how can you represent its state in a clean, professional way? In this article, we'll show you how to include simple graphics in your grid cells.
Figure A: Our form displays On and Off graphics.
Though I didn't want to display 0 or 1 in the Boolean cell, those values are the best way to represent the state internally. When reading the grid data from a disk file, a 0 in that position meant off and a 1 meant on. When internally setting information in the cells, I also stored a 0 or 1--but when the grid was displayed, I intercepted the draw event for those cells and displayed a graphic rather than the underlying 0 or 1. Let's look at how you can duplicate my results.
Figure B: Our example starts with this simple, single-component form.
You'll want C++Builder to draw the rest of the grid as usual but let you take over one of the columns. As with most of the VCL components, a built-in mechanism lets you do custom drawing.
Now, enter the code from Listing A in the TStringGrid's OnDrawCell event handler.
Listing A: OnDrawCell handler
void __fastcall TForm1::
StringGrid1DrawCell
(TObject *Sender, int Col, int Row,
TRect &Rect,
TGridDrawState State)
{
// Only worry about the State column
// (be sure to skip its header cell).
if ( Col != 1 || Row == 0 ) return;
// Draw or remove the green light to
// show circuit state.
if ( StringGrid1->Cells[ Col ]
[ Row ].ToInt() != 0 )
StringGrid1->Canvas->Draw(
Rect.Left, Rect.Top,
GreenLightBmp );
else
StringGrid1->Canvas->Draw(
Rect.Left, Rect.Top, BlankBmp );
}
This method is called immediately before each
cell is drawn. Since you want to handle only one of the columns, the
OnDrawCell code checks which cell is currently being drawn. If it isn't
the Boolean column, you simply return and let C++Builder continue. However, if
it's the Boolean column, you load the appropriate bitmap onto the grid's canvas
at the cell's location. Notice in Listing A that the Col and Row parameters you pass indicate the current cell being drawn. You first check whether the cell being drawn is a Boolean cell (column 1 is Boolean). Then, you check to be sure this isn't a header cell (row 0), since you don't want to mess up the State column header.
To determine which bitmap to display (On or Off), you check the actual text value stored in that cell. If it's 1, you load the On bitmap (GreenLightBmp). If the value is 0, you load the Off bitmap (BlankBmp). The graphics are placed on the grid canvas, since there's no such thing as a cell canvas. The location of the current cell being drawn is also passed into this event handler; you use this location to determine where on the grid canvas to place the graphic. You can create the bitmaps using any imaging package, then save them with a BMP extension. Listing B shows the form's constructor, which fills the grid and loads the bitmaps.
Listing B: Form constructor
__fastcall TForm1::TForm1(TComponent*
Owner) : TForm(Owner)
{
// Load the grid cells.
StringGrid1->Rows[ 0 ]->CommaText =
"Circuit,State";
StringGrid1->Rows[ 1 ]->CommaText =
"C1,0";
StringGrid1->Rows[ 2 ]->CommaText =
"C2,0";
StringGrid1->Rows[ 3 ]->CommaText =
"C3,0";
StringGrid1->Rows[ 4 ]->CommaText =
"C4,0";
// Load the green (on) light bitmap.
// GreenLightBmp is a class data member.
GreenLightBmp = new Graphics::TBitmap;
GreenLightBmp->LoadFromFile(
"GreenLight.bmp" );
// Load the outline (off) light bitmap.
// BlankBmp is a class data member.
BlankBmp = new Graphics::TBitmap;
BlankBmp->LoadFromFile( "Blank.bmp" );
}
Listing C: OnMouseUp event handler
void __fastcall TForm1::
StringGrid1MouseUp
(TObject *Sender,
TMouseButton Button,
TShiftState Shift, int X, int Y)
{
// Get indices of cell
under the mouse.
int col, row;
StringGrid1->MouseToCell( X, Y,
col, row );
// Don't worry about
clicking on header row.
if ( row == 0 ) return;
// If left mouse button
is active, user may
// user may want to mark or unmark
// State field.
if ( Button == mbLeft )
{
// If State column selected,
// to mark (or unmark) the cell.
if ( col == 1 )
{
// If cell is off, turn on.
if ( StringGrid1->Cells[ col ]
[ row ].ToInt() == 0 )
StringGrid1->Cells[ col ]
[ row ] = 1;
else
StringGrid1->Cells[ col ]
[ row ] = 0;
}
}
}
Because you also aren't concerned about the header row (row 0), you check for
it and simply return if the user clicked on that cell. The OnMouseUp
event handler is called for left and right mouse clicks; so, you check to be
sure the left-click is being processed. Finally, as before, you make sure this
cell is in the Boolean column (column 1). To toggle the underlying value, simply change the stored value from a 0 to a 1 or vice-versa. You don't toggle the graphic in this code--the OnDrawCell event handler we discussed earlier toggles the graphic after queueing off the underlying 0 or 1 set here in the OnMouseUp event handler.
You changed the phrase to: "editing can only be enabled or disabled for the entire grid." This means that all you can do with editing is enable or disable it. However, the phrase as written, "editing can be enabled or disabled only for the entire grid," means that editing is enabled or disabled for the entire grid rather than part of it. enabled or disabled only for the entire grid.
Since you must allow editing on other columns, you must use other means to intercept edits in the Boolean column but allow them everywhere else. Listing D shows the code to add to the OnSelectCell event handler, which is called when the user selects a cell using the mouse, arrow, or [Tab] key. By setting the CanSelect parameter, you can effectively turn off editing for individual cells.
Listing D: OnSelectCell handler
void __fastcall TForm1::StringGrid1SelectCell
(TObject *Sender, int Col, int Row,
bool &CanSelect)
{
// Don't let user actually edit the
// underlying value in State column.
// Only allow clicking on green or
// blank light.
if ( Col == 1 ) CanSelect = false;
}
Regarding cell highlighting, note that you may want to set the goRangeSelect
Option property to False. If this option is True (the default), the
Boolean cells won't be selected individually--but they can still fall within a
selected range, which may make the cell highlighting visible.