The data-aware controls I've built up to this point all refer to specific fields of the dataset, so I used a TFieldDataLink object to establish the connection with a data source. Now let's build a data-aware component that works with a dataset as a whole: a record viewer.
Delphi's database grid shows the value of several fields and several records simultaneously. My record viewer component lists all the fields of the current record, using a customized grid. This example will show you how to build a customized grid control and a custom data link to go with it.
In Delphi there are no data-aware components that manipulate multiple fields of a single record without displaying other records. The only two components that display multiple fields from the same table are the DBGrid and the DbCtrlGrid, which generally display multiple fields and multiple records.
The record viewer component I'll describe in this section is based on a two-column grid; the first column displays the table's field names, and the second column displays the corresponding field values. The number of rows in the grid corresponds to the number of fields, with a vertical scroll bar in case they can't fit in the visible area.
The data link you need in order to build this component is a class connected only to the record viewer component and declared directly in the implementation portion of its unit. This is the same approach used by VCL for some specific data links. Here's the definition of the new class:
type TMdRecordLink = class (TDataLink) private RView: TMdRecordView; public constructor Create (View: TMdRecordView); procedure ActiveChanged; override; procedure RecordChanged (Field: TField); override; end;
As you can see, the class overrides the methods related to the principal event—in this case, the activation and data (or record) change. Alternatively, you could export events and then let the component handle them, as the TFieldDataLink does.
The constructor requires the associated component as its only parameter:
constructor TMdRecordLink.Create (View: TMdRecordView); begin inherited Create; RView := View; end;
After you store a reference to the associated component, the other methods can operate on it directly:
procedure TMdRecordLink.ActiveChanged; var I: Integer; begin // set number of rows RView.RowCount := DataSet.FieldCount; // repaint all... RView.Invalidate; end; procedure TMdRecordLink.RecordChanged; begin inherited; // repaint all... RView.Invalidate; end;
The record link code is simple. Most of the difficulties in building this example result from the use of a grid. To avoid dealing with useless properties, I derived the record viewer grid from the TCustomGrid class. This class incorporates much of the code for grids, but most of its properties, events, and methods are protected. For this reason, the class declaration is quite long, because it needs to publish many existing properties. Here is an excerpt (excluding the base class properties):
type TMdRecordView = class(TCustomGrid) private // data-aware support FDataLink: TDataLink; function GetDataSource: TDataSource; procedure SetDataSource (Value: TDataSource); protected // redefined TCustomGrid methods procedure DrawCell (ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); override; procedure ColWidthsChanged; override; procedure RowHeightsChanged; override; public constructor Create (AOwner: TComponent); override; destructor Destroy; override; procedure SetBounds (ALeft, ATop, AWidth, AHeight: Integer); override; // public parent properties (omitted...) published // data-aware properties property DataSource: TDataSource read GetDataSource write SetDataSource; // published parent properties (omitted...) end;
In addition to redeclaring the properties to publish them, the component defines a data link object and the DataSource property. There's no DataField property for this component, because it refers to an entire record. The component's constructor is very important. It sets the values of many unpublished properties, including the grid options:
constructor TMdRecordView.Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink := TMdRecordLink.Create (self); // set numbers of cells and fixed cells RowCount := 2; // default ColCount := 2; FixedCols := 1; FixedRows := 0; Options:= [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRowSizing]; DefaultDrawing := False; ScrollBars := ssVertical; FSaveCellExtents := False; end;
The grid has two columns (one of them fixed) and no fixed rows. The fixed column is used for resizing each row of the grid. Unfortunately, a user cannot drag the fixed row to resize the columns, because you can't resize fixed elements, and the grid already has a fixed column.
An alternative approach could be to have an extra empty column, like the DBGrid control. You could resize the two other columns after adding a fixed row. Overall, though, I prefer my implementation.
I used an alternative approach to resize the columns. The first column (holding the field names) can be resized either using programming code or visually at design time, and the second column (holding the values of the fields) is resized to use the remaining area of the component:
procedure TMdRecordView.SetBounds (ALeft, ATop, AWidth, AHeight: Integer); begin inherited; ColWidths  := ClientWidth - ColWidths; end;
This resizing takes place when the component size changes and when either of the columns change. With this code, the DefaultColWidth property of the component becomes, in practice, the fixed width of the first column.
After everything has been set up, the key method of the component is the overridden DrawCell method, detailed in Listing 17.1. In this method, the control displays the information about the fields and their values. It needs to draw three things. If the data link is not connected to a data source, the grid displays an empty element sign (). When drawing the first column, the record viewer shows the DisplayName of the field, which is the same value used by the DBGrid for the heading. When drawing the second column, the component accesses the textual representation of the field value, extracted with the DisplayText property (or with the AsString property for memo fields).
procedure TMdRecordView.DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState); var Text: string; CurrField: TField; Bmp: TBitmap; begin CurrField := nil; Text := ''; // default // paint background if (ACol = 0) then Canvas.Brush.Color := FixedColor else Canvas.Brush.Color := Color; Canvas.FillRect (ARect); // leave small border InflateRect (ARect, -2, -2); if (FDataLink.DataSource <> nil) and FDataLink.Active then begin CurrField := FDataLink.DataSet.Fields[ARow]; if ACol = 0 then Text := CurrField.DisplayName else if CurrField is TMemoField then Text := TMemoField (CurrField).AsString else Text := CurrField.DisplayText; end; if (ACol = 1) and (CurrField is TGraphicField) then begin Bmp := TBitmap.Create; try Bmp.Assign (CurrField); Canvas.StretchDraw (ARect, Bmp); finally Bmp.Free; end; end else if (ACol = 1) and (CurrField is TMemoField) then begin DrawText (Canvas.Handle, PChar (Text), Length (Text), ARect, dt_WordBreak or dt_NoPrefix) end else // draw single line vertically centered DrawText (Canvas.Handle, PChar (Text), Length (Text), ARect, dt_vcenter or dt_SingleLine or dt_NoPrefix); if gdFocused in AState then Canvas.DrawFocusRect (ARect); end;
In the final portion of the method, the component considers memo and graphic fields. If the field is a TMemoField, the DrawText function call doesn't specify the dt_SingleLine flag, but uses dt_WordBreak flag to wrap the words when there's no more room. For a graphic field, the component uses a completely different approach, assigning the field image to a temporary bitmap and then stretching it to fill the surface of the cell.
Notice that the component sets the DefaultDrawing property to False, so it's also responsible for drawing the background and the focus rectangle, as it does in the DrawCell method. The component also calls the InflateRect API function to leave a small area between the cell border and the output text. The output is produced by calling another Windows API function, DrawText, which centers the text vertically in its cell.
This drawing code works both at run time, as you can see in Figure 17.3, and at design time. The output may not be perfect, but this component can be useful in many cases. To display the data for a single record, instead of building a custom form with labels and data-aware controls, you can easily use this record viewer grid. It's important to remember that the record viewer is a read-only component. It's possible to extend it to add editing capabilities (they're already part of the TCustomGrid class); however, instead of adding this support, I decided to make the component more complete by adding support for displaying BLOB fields.
To improve the graphical output, the control makes the lines for BLOB fields twice as high as those for plain text fields. This operation is accomplished when the dataset connected to the data-aware control is activated. The data link's ActiveChanged method is also triggered by the RowHeightsChanged methods connected to the DefaultRowHeight property of the base class:
procedure TMdRecordLink.ActiveChanged; var I: Integer; begin // set number of rows RView.RowCount := DataSet.FieldCount; // double the height of memo and graphics for I := 0 to DataSet.FieldCount - 1 do if DataSet.Fields [I] is TBlobField then RView.RowHeights [I] := RView.DefaultRowHeight * 2; // repaint all... RView.Invalidate; end;
At this point, you stumble into a minor problem. In the DefineProperties method, the TCustomGrid class saves the values of the RowHeights and ColHeights properties. You could disable this streaming by overriding the method and not calling inherited (which is generally a bad technique), but you can also toggle the FSaveCellExtents protected field to disable this feature (as I've done in the component's code).