Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Delphi Cookbook

You're reading from   Delphi Cookbook Over 60 hands-on recipes to help you master the power of Delphi for cross-platform and mobile development on multiple platforms

Arrow left icon
Product type Paperback
Published in Jun 2016
Publisher
ISBN-13 9781785287428
Length 470 pages
Edition 2nd Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Daniele Teti Daniele Teti
Author Profile Icon Daniele Teti
Daniele Teti
Arrow right icon
View More author details
Toc

Table of Contents (10) Chapters Close

Preface 1. Delphi Basics FREE CHAPTER 2. Becoming a Delphi Language Ninja 3. Knowing Your Friends – the Delphi RTL 4. Going Cross-Platform with FireMonkey 5. The Thousand Faces of Multithreading 6. Putting Delphi on the Server 7. Riding the Mobile Revolution with FireMonkey 8. Using Specific Platform Features Index

Customizing TDBGrid

The adage "A picture is worth a thousand words" refers to the notion that a complex idea can be conveyed with just a single still image. Sometimes, even a simple concept is easier to understand and nicer to see if it is represented by images. In this recipe, we'll see how to customize TDBGrid to visualize a graphical representation of data.

Getting ready

Many VCL controls are able to delegate their drawing, or part of it, to user code. It means that we can use simple event handlers to draw standard components in different ways. It is not always simple, but TDBGrid is customizable in a really easy way. Let's say that we have a class of musicians that have to pass a set of exams. We want to show the percent of exams already passed with a progress bar and, if the percent is higher than 50, there should also be a check in another column. Moreover, after listening to the pieces played at the exams, each musician received votes from an external examination committee. The last column needs to show the mean of votes from this committee as a rating from 0 to 5.

How to do it…

We'll use a special in memory table from the FireDAC library. FireDAC is a new data access library from Embarcadero included in RAD Studio since version XE5. If some of the code seems unclear at the moment, consider the in-memory table as a normal TDataSet descendant, which holds its data only in memory. However, at the end of the section, there are some links to the FireDAC documentation, and I strongly suggest that you read them if you still don't know FireDAC:

  1. Create a brand new VCL application and drop a TFDMemTable, a TDBGrid, a TDataSource, and a TDBNavigator on the form. Connect all the components in the usual way (TDBGrid connected to TDataSource followed by TFDMemTable). Set TDBGrid's font size to 18. This will create more space in the cell for our graphical representation.
  2. Using the TFDMemTable fields editor, add the following fields and then activate the dataset by setting its Active property to True:

    Field name

    Field data type

    Field type

    FullName

    String (size 50)

    Data

    TotalExams

    Integer

    Data

    PassedExams

    Integer

    Data

    Rating

    Float

    Data

    PercPassedExams

    Float

    Calculated

    MoreThan50Percent

    Boolean

    Calculated

  3. Now, add all the columns to TDBGrid by right-clicking and selecting Columns Editor. Then, again right-click and select Add all fields on the resultant window. Then, rearrange the columns as shown here and give a nice title caption:
    • FullName
    • TotalExams
    • PassedExams
    • PercPassedExams
    • MoreThan50Percent
    • Rating
  4. In a real application, we should load real data from some sort of database. However, for now, we'll use some custom data generated in code. We have to load this data into the dataset with the code as follows:
    procedure TMainForm.FormCreate(Sender: TObject);
    begin
      FDMemTable1.AppendRecord(
    ['Ludwig van Beethoven', 30, 10, 4]);
      FDMemTable1.AppendRecord(
    ['Johann Sebastian Bach', 24, 10, 2.5]);
      FDMemTable1.AppendRecord(
    ['Wolfgang Amadeus Mozart', 30, 30, 5]);
      FDMemTable1.AppendRecord(
    ['Giacomo Puccini', 25, 10, 2.2]);
      FDMemTable1.AppendRecord(
    ['Antonio Vivaldi', 20, 20, 4.7]);
      FDMemTable1.AppendRecord(
    ['Giuseppe Verdi', 30, 5, 5]);
      FDMemTable1.AppendRecord(
    ['John Doe', 24, 5, 1.2]); 
    end;
  5. Do you remember? We've two calculated fields that need to be filled in some way. Calculated fields need a form of processing behind them to work. The TFDMemTable, just like any other TDataSet descendant, has an event called OnCalcFields that allows the developer to do so. Create the OnCalcFields event handler on TFDMemTable and fill it with the following code:
    procedure TMainForm.FDMemTable1CalcFields(
    DataSet: TDataSet);
    var
      LPassedExams: Integer;
      LTotExams: Integer;
    begin
      LPassedExams := FDMemTable1.
        FieldByName('PassedExams').AsInteger;
      LTotExams := FDMemTable1.
        FieldByName('TotalExams').AsInteger;
      if LTotExams = 0 then
        FDMemTable1.FieldByName('PercPassedExams').AsFloat := 0
    else
         FDMemTable1.FieldByName('PercPassedExams').AsFloat := LPassedExams / LTotExams * 100;
    
    FDMemTable1.FieldByName('MoreThan50Percent').AsBoolean := FDMemTable1.FieldByName('PercPassedExams').AsFloat > 50;
           end;
         end;
  6. Run the application by hitting F9 (or by going to Run | Run) and you will get the following screenshot:
    How to do it…

    Figure 3.1: A normal form with some data

  7. This is useful, but a bit boring. Let's start our customization. Close the application and return to the Delphi IDE.
  8. Go to the Properties of TDBGrid and set Default Drawing to False.
  9. Now, we've to organize the resources used to draw the grid cells. Calculated fields will be drawn directly using code, but the Rating field will be drawn using a 5-star rating image from 0 to 5. It starts with a 0.5 incremental step (0, 0.5, 1, 1.5, and so on). So, drop TImageList on the form, and set Height as 32 and Width as 160.
  10. Select the TImageList component and open the image list's editor by right-clicking and then selecting ImageList Editor. You can find the needed PNG images in the recipe project folder (ICONS\RATING_IMAGES). Load the images in the correct order as shown here:
    • Index 0 as image 0_0_rating.png
    • Index 1 as image 0_5_rating.png
    • Index 2 as image 1_0_rating.png
    • Index 3 as image 1_5_rating.png
    • Index 4 as image 2_0_rating.png

    Go to TDBGrid events and create the event handler for OnDrawColumnCell. All the customization code goes in this event.

    Include the Vcl.GraphUtil unit, and write the following code in the DBGrid1DrawColumnCell event:

    procedure TMainForm.DBGrid1DrawColumnCell(Sender: TObject;
     const Rect: TRect; DataCol: Integer;
      Column: TColumn; State: TGridDrawState);
    var
      LRect: TRect;
      LGrid: TDBGrid;
      LText: string;
      LPerc: Extended;
      LTextWidth: TSize;
      LSavedPenColor, LSavedBrushColor: Integer;
      LSavedPenStyle: TPenStyle;
      LSavedBrushStyle: TBrushStyle;
      LRating: Extended;
      LNeedOwnerDraw: Boolean;
    begin
      LGrid := TDBGrid(Sender);
      if [gdSelected, gdFocused] * State <> [] then
        LGrid.Canvas.Brush.Color := clHighlight;
    
      LNeedOwnerDraw := (Column.Field.FieldKind = fkCalculated) or Column.FieldName.Equals('Rating');
    
      if LNeedOwnerDraw then
      begin
        LRect := Rect;
        LSavedPenColor := LGrid.Canvas.Pen.Color;
        LSavedBrushColor := LGrid.Canvas.Brush.Color;
        LSavedPenStyle := LGrid.Canvas.Pen.Style;
        LSavedBrushStyle := LGrid.Canvas.Brush.Style;
    
        if Column.FieldName.Equals('PercPassedExams') then
        begin
          LText := FormatFloat('##0', Column.Field.AsFloat) + ' %';
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.FillRect(LRect);
          LPerc := Column.Field.AsFloat / 100 * LRect.Width;
          LGrid.Canvas.Font.Size := LGrid.Font.Size - 1;
          LGrid.Canvas.Font.Color := clWhite;
          LGrid.Canvas.Brush.Color := clYellow;
          LGrid.Canvas.RoundRect(LRect.Left, LRect.Top, Trunc(LRect.Left + LPerc), LRect.Bottom, 2, 2);
          LRect.Inflate(-1, -1);
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.Font.Color := clBlack;
          LGrid.Canvas.Brush.Style := bsClear;
    
          LTextWidth := LGrid.Canvas.TextExtent(LText);
          LGrid.Canvas.TextOut(LRect.Left + (
            (LRect.Width div 2) - (LTextWidth.cx div 2)), LRect.Top + ((LRect.Height div 2) - (LTextWidth.cy div 2)), LText);
        end
        else if Column.FieldName.
    Equals('MoreThan50Percent') then
        begin
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.FillRect(LRect);
          if Column.Field.AsBoolean then
          begin
            LRect.Inflate(-4, -4);
            LGrid.Canvas.Pen.Color := clRed;
            LGrid.Canvas.Pen.Style := psSolid;
            DrawCheck(LGrid.Canvas, 
             TPoint.Create(LRect.Left, 
             LRect.Top + LRect.Height div 2),
             LRect.Height div 3);
          end;
        end
        else if Column.FieldName.Equals('Rating') then
        begin
          LRating := Column.Field.AsFloat;
          if LRating.Frac < 5 then
            LRating := Trunc(LRating);
          if LRating.Frac >= 5 then
            LRating := Trunc(LRating) + 0.5;
          LText := LRating.ToString;
          LGrid.Canvas.Brush.Color := clWhite;
          LGrid.Canvas.Brush.Style := bsSolid;
          LGrid.Canvas.Pen.Style := psClear;
          LGrid.Canvas.FillRect(LRect);
          Inc(LRect.Left);
          ImageList1.Draw(LGrid.Canvas, 
             LRect.CenterPoint.X - (ImageList1.Width div 2),
    		 LRect.CenterPoint.Y - (ImageList1.Height div 2),
    		 Trunc(LRating) * 2);
      end;
      end
      else
        LGrid.DefaultDrawColumnCell(Rect, DataCol, Column, State);
    
      if LNeedOwnerDraw then
      begin
        LGrid.Canvas.Pen.Color := LSavedPenColor;
        LGrid.Canvas.Brush.Color := LSavedBrushColor;
        LGrid.Canvas.Pen.Style := LSavedPenStyle;
        LGrid.Canvas.Brush.Style := LSavedBrushStyle;
      end;
    end;
  11. That's all folks! Hit F9 (or go to Run | Run), and we now have a nicer grid with more direct information about our data:
    How to do it…

    Figure 3.2: The same grid with a bit of customization

How it works…

By setting the DBGrid property DefaultDrawing to False, we told the grid that we want to manually draw all the data into every cell. OnDrawColumnCell allows us to actually draw using standard Delphi code. For each cell we are about to draw, the event handler is called with a list of useful parameters to know which cell we're about to draw and what data we have to read considering the column we are currently drawing. In this case, we want to draw only the calculated columns and the Rating field in a custom way. This is not a rule, but this can be done to manipulate all cells. We can draw any cell in the way we like. For the cells where we don't want to do custom drawing, a simple call method, DefaultDrawColumnCell that passes the same parameters we got from the event and the VCL code will draw the current cell as usual.

Among the event parameters, there is a Rect object (of type TRect) that represents the specific area we're about to draw. There is a column object (of type TColumn) that is a reference to the current column of the grid and a State (of type TGridDrawState) that is a set of the grid cell states (for example, Selected, Focused, HotTrack, and many more). If our drawing code ignores the State parameter, all the cells will be drawn in the same way, and users cannot see which cell or row is selected.

The event handler uses a Pascal Sets Intersect to know whether the current cell should be drawn as a Selected or Focused cell. Refer the following code for better clarity:

  if [gdSelected, gdFocused] * State <> [] then
    Grid.Canvas.Brush.Color := clHighlight;

Tip

Remember that if your dataset has 100 records and 20 fields, OnDrawColumnCell will potentially be called 2000 times! So, the event code must be fast; otherwise, the application will become less responsive.

There's more…

Owner drawing is a really large topic and can be simple or tremendously complex, involving much Canvas-related code. However, often the kind of drawing you need will be relatively similar. So, if you need checks, arrows, color gradients, and so on, check the procedures into the Vcl.GraphUtil unit. Otherwise, if you need images, you could use TImageList to hold all the images needed by your grid, as we did in this recipe for the Rating field.

The good news is that the drawing code can be reused by different kinds of controls, so try to organize your code in a way that allows code reutilization by avoiding direct dependencies to the form where the control is.

The code in the drawing events should not contain business logic or presentation logic. If you need presentation logic, put it in a separate, testable function or class.

You have been reading a chapter from
Delphi Cookbook - Second Edition
Published in: Jun 2016
Publisher:
ISBN-13: 9781785287428
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime