Today I had a need for a grid that looks like this:
It mixes text with two different bitmaps, representing an on/off state. In addition it should allow the user to switch the states using the mouse and the keyboard.
(If you are an experienced component developer, expect to be bored by this article. 😉 )
I started out with a TListView in report view but when it came to switching with the keyboard I realized that I could only select the first column (or the whole row). I’m a fan of using the keyboard, so this was a no no. So I replaced the TListView with a TStringGrid. Adding a few events got me what I wanted:
- OnDrawCell for drawing the bitmaps
- OnMouseDown for intercepting mouse clicks
- OnKeyPress for intercepting keyboard presses
I used a TImageList with two images for the bitmaps and set the cells that represent an on/off state to ‘0’ and ‘1’ (I could have used ‘Y’ and ‘N’ but ‘0’ and ‘1’ nicely correspond to the indices in the ImageList.)
I won’t go into this preliminary solution too much because the next step was to make this easier to use than having to set up the events each time I want to use it.
So, what else can we do? Create a component that descends from TStringGrid of course. But I didn’t want to install it, so I made it an interposer class instead.
TStringGrid = class(Grids.TStringGrid)
procedure SwitchCell(_Col, _Row: Integer);
procedure KeyPress(var Key: Char); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure DrawCell(ACol, ARow: Longint; ARect: TRect;
AState: TGridDrawState); override;
procedure Loaded; override;
TForm1 = class(TForm)
constructor Create(_Owner: TComponent); override;
The code above declares a new type TStringGrid which descends from a class with the same name declared in unit Grids. It adds a field and a few methods.
But first, lets initialize the string grid:
constructor TForm1.Create(_Owner: TComponent);
sg_IdeEnhancements.RowCount := 4;
sg_IdeEnhancements.Cells[0, 0] := 'Form';
sg_IdeEnhancements.Cells[1, 0] := 'Make sizeable';
sg_IdeEnhancements.Cells[2, 0] := 'Store size';
sg_IdeEnhancements.Cells[3, 0] := 'Store position';
sg_IdeEnhancements.Cells[4, 0] := 'ComboboxLines';
sg_IdeEnhancements.Cells[0, 1] := 'Tools Properties';
sg_IdeEnhancements.Cells[1, 1] := '1';
sg_IdeEnhancements.Cells[2, 1] := '1';
sg_IdeEnhancements.Cells[3, 1] := '0';
sg_IdeEnhancements.Cells[4, 1] := '0';
sg_IdeEnhancements.Cells[0, 2] := 'Install Packages';
sg_IdeEnhancements.Cells[1, 2] := '1';
sg_IdeEnhancements.Cells[2, 2] := '1';
sg_IdeEnhancements.Cells[3, 2] := '0';
sg_IdeEnhancements.Cells[4, 2] := '0';
sg_IdeEnhancements.Cells[0, 3] := 'Find Text';
sg_IdeEnhancements.Cells[1, 3] := '0';
sg_IdeEnhancements.Cells[2, 3] := '0';
sg_IdeEnhancements.Cells[3, 3] := '0';
sg_IdeEnhancements.Cells[4, 3] := '1';
As you can see, there isn’t anything special here. We just set the cells to strings.
Now to the more interesting code.
The string grid needs to know where to get its images from. Since I want to use a simple interposer class that should not require any special initialization code in the form, I decided it should get its image list from its owner in the method Loaded, it assumes that it is called ‘TheImageList’.
cmp := Owner.FindComponent('TheImageList');
if Assigned(cmp) and (cmp is TImageList) then
FImageList := TImageList(cmp);
The method Loaded is called by the streaming system after all components of a form have been created and initialized, so it is safe to assume that the image list exists.
Now, that the component has been set up, here is the code for drawing the bitmaps.
procedure TStringGrid.DrawCell(ACol, ARow: Integer; ARect: TRect;
if not Assigned(FImageList) or (ARow = 0) or (ACol in [0, 5]) then
s := Cells[ACol, ARow];
bmp := TBitmap.Create;
if (s = '1') then
xOff := ARect.Left + ((ARect.Right - ARect.Left) - bmp.Width) div 2;
YOff := ARect.Top + ((ARect.Bottom - ARect.Top) - bmp.Height) div 2;
Canvas.Draw(xOff, YOff, bmp);
Originally I had put this code into the OnDrawCell event.
It first checks that the image list is assigned and that the cell to draw is one for which I want to draw an image. It then reads the string from the cell and uses it to determine the bitmap to draw. It could have gone a step further and convert the string to an integer and use that as an index into the image list to select from more than two images. It then gets the image as a bitmap and draws it centered on the cell.
That takes care of painting the grid. The rest is even easier: We want to switch between ‘0’ and ‘1’ using the mouse and the keyboard.
procedure TStringGrid.KeyPress(var Key: Char);
if Key <> ' ' then
For the keyboard, we use the KeyPress method (originally assigned to the OnKeyPress event), check for the space key and call SwitchCell for the current column and row.
For the mouse, we first need to get the cell on which the user clicked. That’s simple because TStringGrid has a method for it: MouseToCell. We then pass column and row to SwitchCell as above.
procedure TStringGrid.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
MouseToCell(X, Y, C, r);
And finally the SwitchCell method. It checks again whether the cell is one we actually want to switch and then changes the cell’s string from ‘1’ to ‘0’ and vice versa.
procedure TStringGrid.SwitchCell(_Col, _Row: Integer);
if (_Row = 0) or (_Col in [0, 5]) then
s := Cells[_Col, _Row];
if s = '1' then
s := '0'
s := '1';
Cells[_Col, _Row] := s;
Pretty easy, wasn’t it? So, why did I write this blog post?
Being a lazy bastard™ I of course googled for a solution first. I didn’t find any ready made examples for what I wanted to do so I thought I’d post one for others who are as lazy as I am. And maybe I will need it myself at a later time, so I can google it again.