Besides writing brand-new custom data-aware components, it's common for Delphi programmers to customize the DBGrid control. The goal for the next component is to enhance the DBGrid with the same kind of custom output I've used for the RecordView component, directly displaying graphic and memo fields. To do this, the grid needs to make the row height resizable, to allow space for a reasonable amount of text and big enough for graphics. You can see an example of this grid at design time in Figure 18.4.
An example of the MdDbGrid component at design time. Notice the output of the graphics and memo fields.
While creating the output was a simple matter of adapting the code used in the record viewer component, setting the height of the grid cells ended up being a very difficult problem to solve. The lines of code you'll see for that operation may be few, but they cost me hours of work!
Unlike the generic grid we've used above, a DBGrid is a virtual view on the dataset—there is no relation between the number of rows shown on the screen and the number of rows of data in the dataset. When you scroll up and down through the data records of the dataset, you are not scrolling up and down through the rows of the DBGrid; the rows are stationary while the data moves from one row to the next to give the appearance of movement. For this reason, the program doesn't try to set the height of an individual row to suit its data, but it sets the height of all the data rows to a multiline height value.
This time the control doesn't have to create a custom data link, because it is deriving from a component that already has a complex connection with the data. The new class has a new property to specify the number of lines of text for each row and overrides a few virtual methods: type
TMdDbGrid = class(TDbGrid) private
procedure SetLinesPerRow (Value: Integer); protected procedure DrawColumnCell(const Rect: TRect; DataCol: Integer;
Column: TColumn; State: TGridDrawState); override; procedure LayoutChanged; override; public constructor Create (AOwner: TComponent); override; published property LinesPerRow: Integer read FLinesPerRow write SetLinesPerRow default 1;
The constructor simply sets the default value for the FLinesPerRow field. Here is the set method for the property:
procedure TMdDbGrid.SetLinesPerRow(Value: Integer); begin if Value <> FLinesPerRow then begin
FLinesPerRow := Value; LayoutChanged; end; end;
The side effect of changing the number of lines is a call to the LayoutChanged virtual method. The system calls this method frequently when one of the many output parameters changes. In the code of this method, the component first calls the inherited version and then sets the height of each row. As a basis for this computation it uses the same formula of the TCustomDBGrid class: the text height is calculated using the sample word Wg in the current font (this text is used because it includes both a full-height uppercase character and a lowercase letter with a descender). Here's the code:
procedure TMdDbGrid.LayOutChanged; var
PixelsPerRow, PixelsTitle, I: Integer; begin inherited LayOutChanged; Canvas.Font := Font;
PixelsPerRow := Canvas.TextHeight('Wg') + 3; if dgRowLines in Options then
Inc (PixelsPerRow, GridLineWidth);
Canvas.Font := TitleFont;
if dgRowLines in Options then
Inc (PixelsTitle, GridLineWidth);
// set number of rows
RowCount := 1 + (Height - PixelsTitle) div (PixelsPerRow * FLinesPerRow);
// set the height of each row DefaultRowHeight := PixelsPerRow * FLinesPerRow; RowHeights  := PixelsTitle; for I := 1 to RowCount - 1 do
RowHeights [I] := PixelsPerRow * FLinesPerRow;
Warning Font and TitleFont are the grid defaults that can be overridden by properties of the individual DBGrid column objects. This component would currently ignore those settings.
The difficult part here was to get the last four statements correct. You can simply set the DefaultRowHeight property, but in that case the title row will probably be too high. At first, I tried setting the DefaultRowHeight and then the height of the first row, but this complicated the code used to compute the number of visible rows in the grid (the read-only VisibleRowCount property). If you specify the number of rows (in order to avoid having rows hidden beneath the lower edge of the grid), the base class keeps recomputing them.
Finally, here's the code used to draw the data, ported from the RecordView component and adapted slightly for the grid:
procedure TMdDbGrid.DrawColumnCell (const Rect: TRect; DataCol: Integer;
Column: TColumn; State: TGridDrawState); var
Bmp: TBitmap; OutRect: TRect; begin if FLinesPerRow = 1 then inherited DrawColumnCell(Rect, DataCol, Column, State) else begin // clear area Canvas.FillRect (Rect); // copy the rectangle OutRect := Rect; // restrict output InflateRect (OutRect, -2, -2); // output field data if Column.Field is TGraphicField then begin
DrawText (Canvas.Handle, PChar (Column.Field.AsString),
Length (Column.Field.AsString), OutRect, dt_WordBreak or dt_NoPrefix)
end else // draw single line vertically centered
DrawText (Canvas.Handle, PChar (Column.Field.DisplayText), Length (Column.Field.DisplayText), OutRect, dt_vcenter or dt_SingleLine or dt_NoPrefix);
In the code above you can see that if the user displays just a single line, the grid uses the standard drawing technique with no output for memo and graphic fields. However, as soon as you increase the line count, you'll see a better output.
To see this code in action, run the GridDemo example. This program has two buttons you can use to increase or decrease the row height of the grid, and two more buttons to change the font. This is an important test because the height in pixels of each cell is the height of the font multiplied by the number of lines.
Was this article helpful?