September 1998

Using TStringGrid with graphics

by Gerry Myers

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.

Visual indicators

Recently, I worked a project that contained a Boolean column like that I just described. The TStringGrid satisfied all my other display requirements, since the remaining data was text. At first I thought I'd show a 0 or 1 in the Boolean cells--but then I'd have to check every user input to be sure it wasn't something other than 0 or 1. Instead, I decided to place an indicator-light graphic in those cells and let the user click on them. The graphic toggles between an on and off indication, as shown in Figure A (the indicator is actually green).

Figure A: Our form displays On and Off graphics.
[ Figure A ]

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.

 

OnDrawCell

To begin, open a new C++Builder project. Place a two-column TStringGrid component on the form, as shown in Figure B.

Figure B: Our example starts with this simple, single-component form.
[ Figure B ]

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" );
}

OnMouseUp

So far, you have a grid that will display different cell graphics depending on the underlying values in the cells. This is fine for programs that change the 0 and 1 internally. However, you'll also want to let users click on the indicator light and toggle the underlying value. You can do so fairly easily by overriding the OnMouseUp event handler. As Listing C shows, this time the column and row aren't passed in as parameters. Therefore, you find the position of the mouse, then use the MouseToCell() method to convert this location to the coordinates of the grid cell that the mouse is over.

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.

 

OnSelectCell

Again, you could walk away now and have a functioning grid that lets the user click on the graphic to toggle its display and underlying value. There's just one cosmetic problem: Since editing is allowed and highlighting is turned on, a blue highlight background appears in the cell if the bitmap doesn't extend all the way to the cell edges. For example, if the user widens the column so the cell is wider or taller than the bitmap, the highlighted background will show. You could solve this problem by stretching the bitmap instead of simply drawing it onto the cell. However, doing so will probably distort the graphic. Another thought would be to disable editing on that column--but editing can be Watch the use of "only" -- its placement affects meaning.

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.

 

Conclusion

The TStringGrid component is very useful. With the ability to create user drawings via the OnDrawCell event handler, the component can serve many project needs. In this article, we demonstrated how to display graphics in grid cells. You can easily expand on our discussion to incorporate buttons, checkboxes, combo boxes, and other elements in your grids, expanding their usefulness even further.