How do I highlight a row in a TDBGrid based on the value of field?
This question begs for a short bit of a philosophical discourse before I actually answer the question. Really, I'm not just tooting my horn... it's actually valid
Cue It Up
Providing visual cues for users in your applications is an important part of good interface design. Visual cues allow the user to immediately identify and differentiate special circumstances that may occur within the course of an application’s session. For instance, in a column of numbers, it’s easier to tell the difference between positive and negative values at first glance if the negative numbers are either a different color or enclosed in parentheses. In the table below, the left column contains positive and negative numbers with the negative number merely represented by their negative sign. The right column represents negative numbers in red boldface text. Let's take a look.
10015
15486
-54862
54846
78948
40594
-45945
78945 10015
15486
-54862
54846
78948
40594
-45945
78945
As you can see, it is far easier to discern the negative values in the right column than it is to discern them in the left column.
The point to all this is that if you build programs or interfaces which will be viewed by a lot of people, it's a good idea to do the extra bit of work to make the job of interpreting what's in your interface that much easier.
Let's Get Down to Business
See? I told you it wasn't going to take a long time... In any case, let's get back to the topic... Highlighting a row in a TDBGrid based on the value of a particular field is a perfect example of providing visual cues for your users. But with this particular operation, it's not as intuitive as you might think; not that it's hard, but I had to do a bit of digging to get the answer for this one.
At first glance, I thought why not just put some code in the OnCalcFields event. Okay, okay... bad thought. So lo and behold I saw the OnDrawDataCell event. Unfortunately, I found out that OnDrawColumnCell replaced it in the 32-bit versions of Delphi, so I had to turn that way. No problem. I found a bit of code in Neil Rubenking's book, "Delphi Programming Problem Solver," and that got me started. Let's talk concepts first...
Behind the Scenes
I didn't know much detail information about the TDBGrid component until I studied the VCL source code in DBGrids.PAS. And what I discovered was rather interesting. To draw itself, TDBGrid performs a lot of canvas manipulation. And while it happens rather fast, each cell in the grid is drawn individually. Essentially, the grid gets the dimensions of each cell and feeds that information into a TRect structure. Then that is passed to various drawing functions to display the text. Yikes! You ought to see the DrawCell method - it's big! Good for us that it's pretty fast at executing...
Drawing the Cell
As it turns out, the way painting occurs is pretty straight forward. Delphi progresses through the grid row-by-row. In each row, it then iterates through each cell, drawing the background and text within the cell's bounding rectangle. So, to highlight a row, we actually have to highlight each cell of the row, and that's handled through the OnDrawColumnCell event and a little code.
Study the code sample below, and we'll discuss it immediately after the listing:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect; DataCol: Integer; Column: TColumn;
State: TGridDrawState);
begin
with Sender as TDBGrid, DataSource.DataSet do
if (FieldByName('AmountPaid').AsFloat > 15000) then
DrawField(Column.Field.DisplayText, Rect, Canvas,
Column.Font, Column.Alignment, [fsBold],
clYellow, clRed);
end;
{This is the workhorse routine that does the drawing.}
procedure TForm1.DrawField(const Value : String;
const Rect : TRect;
vCanvas : TCanvas;
vFont: TFont;
vAlignment: TAlignment;
FontStyle : TFontStyles;
FontColor : TColor;
BGColor : TColor);
var
I : Integer;
begin
I := 0;
//First, fill in the background color of the cell
vCanvas.Brush.Color := BGColor;
vCanvas.FillRect(Rect);
//SetBkMode ensures that the background is transparent
SetBkMode(Canvas.Handle, TRANSPARENT);
//Set the passed font properties
vCanvas.Font := vFont;
vCanvas.Font.Color := FontColor;
vCanvas.Font.Style := vCanvas.Font.Style + FontStyle;
//Set Text Alignment
case vAlignment of
taRightJustify :
begin
SetTextAlign(vCanvas.Handle, TA_RIGHT);
I := Rect.Right - 2;
end;
taLeftJustify :
begin
SetTextAlign(vCanvas.Handle, TA_LEFT);
I := Rect.Left + 2;
end;
taCenter :
begin
SetTextAlign(vCanvas.Handle, TA_CENTER);
I := (Rect.Right + Rect.Left) DIV 2;
end;
end; { case }
//Draw the text
vCanvas.TextRect(Rect, I, Rect.Top + 2, Value);
SetTextAlign(vCanvas.Handle, TA_LEFT);
end;
The code above is an excerpt from a form unit that I created. On the form, I've got a TTable, TDataSource, and a TDBGrid dropped onto it. The TTable points to the DBDEMOS alias, and is linked to the ORDERS.DB table. It's really simple.
Anyway, what's going in the code? What we're doing in the event handler code is merely adding a little functionality to the default cell drawing methodology by calling the DrawField method of the Form1. Since we're riding on the assumption that the DBGrid iterates row-by-row, while at the same time checking and refreshing its datalinks, we can use that to check the value of a field as DBGrid iterates - in this case, the AmountPaid field - to see if we need to highlight the row. I won't go into specific details about the DataField method. It's pretty straight-forward. However, I will point out the most important thing, and that's the first part of the method:
//First, fill in the background color of the cell
vCanvas.Brush.Color := BGColor;
vCanvas.FillRect(Rect);
//SetBkMode ensures that the background is transparent
SetBkMode(Canvas.Handle, TRANSPARENT);
Here, I'm filling in the background of the cell. If I didn't call SetBkMode immediately after the FillRect method, the row would show up completely solid with no text displayed. In any case, try this out and see how it works for you. Well... we're actually not done yet....
I Just Can't Get No Satisfaction
After I wrote this code, I just wasn't satisfied with it. Why? The reason is because I take object-oriented programming seriously, and I realized that the proper way to introduce this type of functionality into a DBGrid would be to actually make a component that had the capability built into it. That way, I wouldn't have to rewrite the code each time I wanted to have this functionality. So that's what I did...
NOTE: If you have the InfoPower components by Woll2Woll software, the TwwDBGrid has this functionality PLUS lots of other really neat enhancements. If you don't have them, you should consider buying them. So why would I bother writing an article on this? Well... why not?
The code listing below is the complete listing of TEnhDBGrid. I'll talk particulars after I give you the listing.
unit EnhDBGrid;
interface
uses
Windows, Classes, Graphics, Grids, DBGrids;
type
TEnhDBGrid = class(TCustomDBGrid)
private
FHighlightBGColor : TColor;
FHighlightFont : TFont;
FDoRowHighlight : Boolean;
procedure DrawField(const Value : String;
const Rect : TRect;
vCanvas : TCanvas;
const vFont: TFont;
vAlignment: TAlignment;
const FontStyle : TFontStyles;
const FontColor : TColor;
const BGColor : TColor);
protected
procedure DrawColumnCell(const Rect: TRect;
DataCol: Integer;
Column: TColumn;
State: TGridDrawState); override;
procedure SetHighlightFont(Value : TFont);
public
constructor Create(AOwner : TComponent); override;
property Canvas;
property SelectedRows;
property DoRowHighLight : Boolean read FDoRowHighLight
write FDoRowHighlight
default False;
published
property Align;
property BorderStyle;
property Color;
property Columns stored False; //StoreColumns;
property Ctl3D;
property DataSource;
property DefaultDrawing;
property DragCursor;
property DragMode;
property Enabled;
property FixedColor;
property Font;
property HighlightBGColor: TColor read FHighlightBGColor
write FHighlightBGColor;
property HighlightFont : TFont read FHighlightFont
write SetHighlightFont;
property ImeMode;
property ImeName;
property Options;
property ParentColor;
property ParentCtl3D;
property ParentFont;
property ParentShowHint;
property PopupMenu;
property ReadOnly;
property ShowHint;
property TabOrder;
property TabStop;
property TitleFont;
property Visible;
property OnCellClick;
property OnColEnter;
property OnColExit;
property OnColumnMoved;
property OnDrawDataCell; { obsolete }
property OnDrawColumnCell;
property OnDblClick;
property OnDragDrop;
property OnDragOver;
property OnEditButtonClick;
property OnEndDrag;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnStartDrag;
property OnTitleClick;
end;
procedure Register;
implementation
constructor TEnhDBGrid.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
//Give the highlight font a default value
FHighlightFont := TFont.Create;
end;
procedure TEnhDBGrid.DrawField(const Value : String;
const Rect : TRect;
vCanvas : TCanvas;
const vFont: TFont;
vAlignment: TAlignment;
const FontStyle : TFontStyles;
const FontColor : TColor;
const BGColor : TColor);
var
I : Integer;
begin
I := 0;
//First, fill in the background color of the cell
vCanvas.Brush.Color := BGColor;
vCanvas.FillRect(Rect);
//SetBkMode ensures that the background is transparent
SetBkMode(Canvas.Handle, TRANSPARENT);
//Now draw out the text in the cell
vCanvas.Font := vFont;
vCanvas.Font.Color := FontColor;
vCanvas.Font.Style := vCanvas.Font.Style + FontStyle;
//Now set the text alignment
case vAlignment of { }
taRightJustify :
begin
SetTextAlign(vCanvas.Handle, TA_RIGHT);
I := Rect.Right - 2;
end;
taLeftJustify :
begin
SetTextAlign(vCanvas.Handle, TA_LEFT);
I := Rect.Left + 2;
end;
taCenter :
begin
SetTextAlign(vCanvas.Handle, TA_CENTER);
I := (Rect.Right + Rect.Left) DIV 2;
end;
end; { case }
//Write the text!!!
vCanvas.TextRect(Rect, I, Rect.Top + 2, Value);
//Necessary step to align rest of the text in the DBGrid
SetTextAlign(vCanvas.Handle, TA_LEFT);
end;
procedure TEnhDBGrid.DrawColumnCell(const Rect: TRect;
DataCol: Integer; Column: TColumn;
State: TGridDrawState);
begin
//Do the inherited method
inherited DrawColumnCell(Rect, DataCol, Column, State);
//If user wants row highlighted, then call DrawField
if DoRowHighLight then
DrawField(Column.Field.DisplayText, Rect, Canvas,
FHighlightFont, Column.Alignment,
FHighlightFont.Style,
FHighlightFont.Color, FHighlightBGColor);
end;
procedure TEnhDBGrid.SetHighlightFont(Value : TFont);
begin
//Assign the font
FHighlightFont.Assign(Value);
end;
procedure Register;
begin
RegisterComponents('BD', [TEnhDBGrid]);
end;
end.
The component is merely a functionality wrapper for the form code above, so I won't discuss it in any meaningful detail. What I will discuss, however are the properties that I introduced into the component. First off is the public property DoRowHighlight. This is a Boolean property that you set at runtime to activate the functionality of the component. You use it in the OnDrawColumnCell method as follows:
procedure TForm1.EnhDBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect; DataCol: Integer; Column: TColumn;
State: TGridDrawState);
begin
//Turns highlighting on or off depending on the row
with Sender as TEnhDBGrid, DataSource.DataSet do
if FieldByName('AmountPaid').AsFloat > 15000 then
DoRowHighlight := True
else
DoRowHighlight := False;
end;
This is similar to what I did in the example above, but in this case, depending upon the AmountPaid field's value, I turn the functionality on or off. Admittedly, it's rather crude, but hey! it works!
The other two properties are HighlightFont and HighlightBGColor. The first property merely sets the highlighted cells' font properties and the second sets the color of the background you want to paint. With respect to the HighlightFont property, notice in the Create constructor, I actually create an instance of a TFont and assign it to FHighlightFont. This is necessary, otherwise at runtime, you won't be able to set the font to anything. It's possible to assign the font to the default font of the DBGrid, but you won't be able to change the font. So, creating an instance that can be changed is the way to go.
Those of you who are pros at component writing may notice something in the code. The DrawField method is declared as virtual. The reason for this is that just in case someone wanted to add functionality to what I already created, they wouldn't have to rewrite the code - it's adding a bit of an OOP twist to the component.
Also, notice that I didn't descend from TDBGrid; rather, I descended directly from TDBGrid. My reasoning behind this was that TDBGrid merely exposes the protected properties of TCustomDBGrid. I felt it was a waste to branch off of a component that didn't really add any new functionality. Also, while I exposed the same properties as TDBGrid does, I could have just as easily limited the exposure. In other words, descending from the ancestor above TDBGrid gives me a lot more control over what I want to expose and not expose.
Well, thanks for bearing with me. Have fun with the code!