Working with string grids
by Damon Chandler
String grids are ideal for displaying large amounts of data in a compact, tabular format. Conceptually, a string grid is little more than a graphical representation of a two-dimensional array of strings. Despite this conceptual simplicity, working with the TStringGrid class can sometimes be intimidating.
In this article, I’ll get you up to speed on using the TStringGrid class effectively. I’ll start with a brief overview of where the class lies in the VCL hierarchy, and then discuss a few of its most commonly used properties. Next, I’ll explore some of the more practical aspects of the TStringGrid class: I’ll demonstrate how to render the text of each cell in a customized format, how to insert and remove a column or row, and how to save and load the cells to and from a file.
The ancestor classes
The TStringGrid class is a direct descendant of the TDrawGrid class, which is itself a descendant of the TCustomGrid class—the base class for all standard grid controls. The TCustomGrid class is a TCustomControl descendant that provides the basic functionality of a stock grid-type control.
The TDrawGrid class adds little to its parent class, introducing only the CellRect() and MouseToCell() methods. Also introduced are several events, whose handlers are simply invoked in response to corresponding inherited methods. For example, the OnDrawCell event handler is called from within the DrawCell() method. As the TDrawGrid class acquires nearly all of its functionality from the TCustomGrid class, it’s no surprise that the implementation of the former contains less than one hundred lines of code. The implementation of its parent class, on the other hand, contains nearly thirty times that amount. In short, a TDrawGrid object is essentially an “exposed” version of a TCustomGrid object.
The TStringGrid class extends the TDrawGrid class by providing support for per-cell AnsiString and application-defined data association. The strings are exposed via the Cells, Cols, and Rows properties, while the data is coupled via the Objects property.
The Cells, Cols, and Rows properties
As mentioned, each cell in a string grid can be assigned an AnsiString value. This functionality is provided through the Cells, Cols, and Rows properties.
The Cells property affords the most common and intuitive means of manipulating the contents of a particular cell. This two-dimensional array-type property is declared as follows:
__property AnsiString Cells
[int ACol][int ARow] = {
read=GetCells, write=SetCells};
The ACol parameter indicates the zero-based index of the column to which the target cell belongs, while the ARow parameter indicates the zero-based index of the target cell’s row. For example, to assign a value to the first (top-leftmost), non-fixed cell, you use the Cells property as such:
const int target_col = StringGrid1->FixedCols; const int target_row = StringGrid1->FixedRows; StringGrid1->Cells[target_col] [target_row] = "New String";
The FixedCols and FixedRows properties indicate the number of fixed columns and rows, respectively.
To extract the value of a particular cell, you read the Cells property in a similar fashion. For example, the following code displays the contents of the last (bottom-rightmost) cell:
const int target_col = StringGrid1->ColCount - 1; const int target_row = StringGrid1->RowCount - 1; ShowMessage(StringGrid1->Cells [target_col][target_row]);
The ColCount and RowCount properties correspond to the number of columns and rows (including the fixed cells), respectively.
The Cols and Rows properties provide access to an entire column or row of strings, respectively. Each of these properties is published as an array of TStrings*:
__property Classes::TStrings*
Cols[int Index] = {
read=GetCols, write=SetCols};
__property Classes::TStrings*
Rows[int Index] = {
read=GetRows, write=SetRows};
To access the contents of a particular cell from the return value of the Cols or Rows property, you use the Strings property, specifying the target row or column. For example, to access the contents of fourth cell in third column using the Cols property:
const int target_col = 2; const int target_row = 3; ShowMessage(StringGrid1->Cols [target_col]->Strings[target_row]);
And, to access the contents of the same cell using the Rows property:
const int target_col = 2; const int target_row = 3; ShowMessage(StringGrid1->Rows [target_row]->Strings[target_col]);The number of entries contained in the TStrings object returned via the Cols(Rows) property is identically RowCount(ColCount). So, in our first example using the Cols property, target_row can range from zero to RowCount. Similarly, for our second example using the Rows property, target_col can range from zero to ColCount.
The Objects property
The Objects property is the TObject* equivalent of the Cells property. That is, instead of representing a two-dimensional array of AnsiStrings, the Objects property stores a pointer to any application-defined data. In this way, you can associate extra information with each cell. The property is declared as follows:
__property System::TObject*
Objects[int ACol][int ARow] = {
read=GetObjects, write=SetObjects};
You access the Objects property in the same way as you would the Cells property. For example, to associate an integral value with the first (top-leftmost), non-fixed cell, you use the Objects property as such:
const int target_col =
StringGrid1->FixedCols;
const int target_row =
StringGrid1->FixedRows;
const int associated_data = 1001;
StringGrid1->Objects
[target_col][target_row] =
reinterpret_cast
<TObject*>(associated_data);
Also, note that the TStringGrid class does not automatically free the memory associated with any data assigned to its Objects property. Therefore, if you indeed assign the Objects property a pointer to an instantiated object, be sure to handle the memory de-allocation appropriately.
Controlling the text alignment
Not only does the TStringGrid class handle the storage of each cell’s string and data, it also provides automatic rendering of the text. This task is accomplished from within the redefinition of the inherited DrawCell() method, where the TextRect() method is employed. However, you may want to have the contents of each cell formatted in a personalized fashion. For example, if your string grid contains a column that holds monetary values, you might prefer the text of these cells to be aligned to the right.
While the TStringGrid class does not provide a direct means of controlling the text alignment of its cells, it does inherit the OnDrawCell event from the TDrawGrid class. By providing a handler for this event, you can render the cell’s contents justified to the left, center, or right side of the cell.
The OnDrawCell event
As mentioned, the OnDrawCell event is inherited from the TDrawGrid class where it is declared as follows:
__property TDrawCellEvent OnDrawCell =
{read=FOnDrawCell, write=FOnDrawCell};
TDrawCellEvent is a custom event type designed specifically for rendering grid-type controls:
typedef void __fastcall (__closure *TDrawCellEvent) (System::TObject* Sender, long ACol, long ARow, Windows::TRect Rect, TGridDrawState State);
The Sender parameter is a pointer to the string grid itself. The ACol and ARow parameters indicate the current column and row of the cell that requires rendering, respectively, while the Rect parameter identifies the bounding rectangle of this cell (in client coordinates). The State parameter is a set that indicates the state of the current cell. You decode the members of the State parameter to determine how the cell should be rendered. This parameter can contain a combination of the following self-explanatory values: gdSelected, gdFocused, and gdFixed.
Rendering the text
In addition to the OnDrawCell event, the TStringGrid class inherits the DefaultDrawing property, which is first introduced in the TCustomGrid class. When this property is set to true (the default), the TCustomGrid class handles all of the “default” rendering. That is, each cell is automatically rendered according to the members of the State parameter. In this case, within a handler for the OnDrawCell event, you are only required to render the formatted text. For example, to render the contents of each cell in a right-justified fashion:
#include <cassert>
void __fastcall
TForm1::StringGrid1DrawCell(
TObject *Sender, int ACol, int ARow,
TRect &Rect, TGridDrawState State)
{
TStringGrid* StringGrid =
static_cast<TStringGrid*>(Sender);
assert(StringGrid != NULL);
StringGrid->Canvas->FillRect(Rect);
AnsiString text(
StringGrid->Cells[ACol][ARow]);
RECT RText = static_cast<RECT>(Rect);
InflateRect(&RText, -3, -3);
DrawText(StringGrid->Canvas->Handle,
text.c_str(), text.Length(), &RText,
DT_RIGHT | DT_SINGLELINE |
DT_VCENTER
);
}
The DrawText() API function is ideal for rendering justified text. As in the preceding example, the last parameter to this function can be specified as a combination of several text-formatting values. For left-justification, you simply replace the DT_RIGHT flag with DT_LEFT. Likewise, for center justification, use the DT_CENTER flag.
In fact, you can use this technique to adjust the color or style of the text as well. In such a case, you simply manipulate the Font property of the string grid’s Canvas.
Inserting or removing a row or column
While the ColCount and RowCount properties can be used to adjust the number of columns and rows, respectively, there is no built-in technique to insert a column or row at a specific location. For example, if you need to append a row to the end of the string grid, you simply increment the RowCount property by one. However, if you need to insert a row at, for instance, the middle of the grid, the RowCount property alone will not help.
As you may have guessed, inserting a column or row actually requires two steps: (1) Append a column or row to the end of the grid, then (2) shift the contents of each trailing (i.e., located after the insertion location) column or row. For example, if your grid contains five rows and you need to insert a new row between the first and second rows, you must first add a new row to the end of the grid, then shift the contents of the second through fifth rows down by one. This process can be accomplished via the following approach:
void __fastcall InsertRow(
TStringGrid* StringGrid,int AfterIndex)
{
SendMessage(StringGrid->Handle,
WM_SETREDRAW, false, 0);
try {
const int row_count =
StringGrid->RowCount;
// (1) append a new row to the end
StringGrid->RowCount = row_count +1;
// (2) shift the contents
// of the trailing rows
for (int row = row_count;
row > AfterIndex + 1; --row) {
StringGrid->Rows[row] =
StringGrid->Rows[row - 1];
}
StringGrid->Rows[
AfterIndex + 1]->Clear();
}
catch (...) {
SendMessage(StringGrid->Handle,
WM_SETREDRAW, true, 0);
}
SendMessage(StringGrid->Handle,
WM_SETREDRAW, true, 0);
// update (repaint) the shifted rows
RECT R =
StringGrid->CellRect(0, AfterIndex);
InflateRect(&R, StringGrid->Width,
StringGrid->Height);
InvalidateRect(
StringGrid->Handle, &R, false);
}
The WM_SETREDRAW message is used to temporarily prevent the string grid from repainting itself during the manipulation of its contents. After the new row has been inserted, the InvalidateRect() API function is used to force the string grid to repaint the shifted cells. A similar approach can be employed to insert a new column.
Removing a particular column or row also requires a two-step process, but in the reverse order. This time, you first shift the contents of the trailing columns or rows, then remove the last column or row. For example:
void __fastcall RemoveCol(
TStringGrid* StringGrid, int Index)
{
SendMessage(StringGrid->Handle,
WM_SETREDRAW, false, 0);
try {
const int col_count =
StringGrid->ColCount;
// (1) shift the contents of
// the trailing columns
for (int col = Index;
col < col_count - 1; ++col) {
StringGrid->Cols[col] =
StringGrid->Cols[col + 1];
}
// (2) remove the last column
StringGrid->ColCount = col_count -1;
}
catch (...) {
SendMessage(StringGrid->Handle,
WM_SETREDRAW, true, 0);
}
SendMessage(StringGrid->Handle,
WM_SETREDRAW, true, 0);
// update (repaint) the shifted cols
RECT R =
StringGrid->CellRect(0, Index);
InflateRect(&R, StringGrid->Width,
StringGrid->Height);
InvalidateRect(StringGrid->Handle,
&R, false);
}
As it turns out, the TCustomGrid class contains the protected virtual DeleteColumn() and DeleteRow() methods. These methods can be used to remove a particular column or row, respectively. Therefore, as an alternative to the manual approach, you could create your own TStringGrid descendant class that provides public access to these protected methods. For example:
class TMyStringGrid : public TStringGrid
{
public:
__fastcall TMyStringGrid(
TComponent* AOwner) :
TStringGrid(AOwner) {}
void __fastcall RemoveCol(int AIndex) {
TStringGrid::DeleteColumn(AIndex);
}
void __fastcall RemoveRow(int AIndex) {
TStringGrid::DeleteRow(AIndex);
}
};
Persistent cells
Rarely is a string grid used for the display of interim data. In most cases, you’ll want to provide your consumers with the ability to save and move their work to and from a file. While the TStringGrid class does not provide an automated means of accomplishing this task, it is not difficult to implement manually. In fact, since the Cols and Rows properties are both published as type TStrings*, you can delegate most of the work to this latter class.
Writing the cells to a file
The TStrings class provides the SaveToFile() method that can be used to write its contained strings to a file. To use this method with a string grid, first create a temporary TStringList object that will serve as a container of all the cells. Next, iterate over the Cols (or Rows) property, adding the contents of each column (or row) to the temporary container via the AddStrings() method. Finally, call the SaveToFile() method to write the contents of the temporary container to a file. For example:
#include <memory>
void __fastcall SaveCells(
TStringGrid* StringGrid,
const AnsiString& FileName)
{
std::auto_ptr<TStrings> SaveStrings(
new TStringList());
const int col_count =
StringGrid->ColCount;
for (int index = 0;
index < col_count; ++index) {
SaveStrings->AddStrings(
StringGrid->Cols[index]);
}
SaveStrings->SaveToFile(FileName);
}
Loading the cells from a file
To load a string grid with text from a file created via the above SaveCells() function, you perform a similar operation. Again, make use of the TStrings class; this time, the LoadFromFile() method. First, create a temporary TStringList object that will hold the contents of the file. Next, call the LoadFromFile() method to fill this container, then iterate over the TStringGrid’s Cells property, assigning each cell its corresponding entry from the container. For example:
#include <memory>
void __fastcall LoadCells(
TStringGrid* StringGrid,
const AnsiString& FileName)
{
std::auto_ptr<TStrings> LoadStrings(
new TStringList());
LoadStrings->LoadFromFile(FileName);
int index = 0;
const int col_count =
StringGrid->ColCount;
const int row_count =
StringGrid->RowCount;
for (int col=0;col<col_count;++col)
{
for (int row=0;row<row_count;++row)
{
StringGrid->Cells[col][row] =
LoadStrings->Strings[index++];
}
}
}
As an exercise, modify the above LoadCells() function to make use of the WM_SETREDRAW message in order to prevent repainting during loading.
Conclusion
String grids provide a great opportunity for representing data in a tabular format. Perhaps this demonstration of these techniques for accomplishing the most fundamental string grid-related tasks has convinced you that, even though the class lacks certain basic features, it is flexible enough to allow for easy enhancement. From there, the possibilities are practically endless.