Adding controls to a DBGrid

by Steve Ketcham

The standard TDBGrid does its job, but sometimes the default behavior is not exactly what you want. Boolean fields, for example, show either “True” or “False” depending on the value of the data field. In this case, it is much more visually appealing to be able to you use a check box rather than the standard display. Put another way, it would be better if the grid could show a TDBCheckBox rather than “True” or “False”. Or maybe you want to be able to show a customized combo box or other control on a grid. You can add these controls to a TDBGrid. You only need to handle three events and write three worker functions.

 

Defining the process

When adding controls to DBGrid there are a couple of questions to be answered and several requirements:

 

  1. If the control is for visual effect, as in the case of a TDBCheckBox, then the grid should draw the appropriate image in the correct cell after the control is no longer visible. The drawing event should also allow for the user to change column widths. If the control is not being used for visual reasons these are not required .

  2. If there is more than one added control, there must be a way to select the correct control.

  3. When the grid’s SelectedField is the same as the corresponding control’s DataField, the control should receive focus and become visible. The opposite is also true. When the grid’s SelectedField is not the same as the control’s DataField, the control should lose focus and become invisible. While some controls are easy to add, others need to be doctored so they look right. For example, a TDBCheckBox does not have taCenterJustify value in its Alignment property, only taLeftJustify and taRightJustify. This makes it is hard to center in a grid cell.

  4. Is this going to be a component, a new class or just cut and paste code for occasional use? This really is not “everyday use” coding: how often would you use it? Some people might use it a lot, most probably would not. If you intend to use this code often, you should consider writing a component to do the work.

 

Rather than writing a component, this is going to be cut and paste code since the example described is not very detailed. The idea is to show how to integrate controls. However, this code could easily be made into a class or component, thereby making it reusable.

Now that I have outlined what needs to be done, I’ll explain the process of adding a TDBCheckBox to a grid.

 

Step one: add controls

Add a TDBGrid to a form and connect the grid to the appropriate TDataSource and TTable. Make sure the TTable is connected to a database that contains a Boolean field (the VENDORS.DB table that ships with C++Builder is a good choice if you don’t have a table of your own to use). Next place a TDBCheckbox on the form and set its Visible property to false.

Set the Name property of the TDBCheckbox to DBCheckBoxGrid. Change the color of DBCheckBoxGrid to clWindow so it blends in with the grid and remove any captions. If your grid is not the default color, change the control appropriately. One last thing: make sure the TDBCheckBox is connected to the same TDatasource as the grid and to the correct field.

Step two: create a function to draw the checked or unchecked state

This is the first worker function. A TDBCheckBox is normally connected to a Boolean field. The TDbCheckBox you placed on the form is going to be added to the DBGrid for visual effect. This means you need at least two images for drawing: one for the checked state, and one for the unchecked state. The easiest way to accomplish this is to use the Windows API DrawFrameControl() function to draw directly on the grid’s canvas(that’s how Windows does it!). Declare a function in the private section of your main form with this declaration:

 

void SetCheck(

const TRect &Rect, bool IsChecked);

 

This function needs to know two things: whether it should draw the mark as checked or unchecked, and where to draw the mark. The OnDrawColumnCell event handler for the grid (which we haven’t written yet) will call this function (see Listing A).

Inside the SetCheck() function is another function called NewDimensions(). This function is declared as follows:

 

int NewDimensions(

int Rect1, int Rect2, int Dimension);

 

This is the second worker function. NewDimensions() will provide the value needed to center DBCheckBoxGrid if it has focus or to center the check mark correctly if required. NewDimensions() also takes care of automatically centering the check mark (see Listing F).

 

Step three: find the correct control

This is a little out of order because I still have not finished addressing the grid’s cell drawing code but I will do that shortly. Create another function in the main form with this declaration:

 

TWinControl* FindGridControl(

AnsiString FieldName, bool IsVisible);

 

This function takes a field name and returns a TWinControl pointer to the correct control. It also sets the Visible property of the control. Inside the function the Parent property of the control is changed to match the grid’s Parent property. This gives the control and the grid a common reference for positioning the control. This is explained a little later.

The FindGridControl() method performs two different functions. It determines whether the grid needs to paint check marks for a particular column, and it allows the grid to give focus to the control and hide the control when it is not needed. Listing D shows this function. This code is only necessary if you use more than one control on a grid or if you want to reuse DBCheckBoxGrid for another Boolean field. Generally speaking, this will be the only function that will ever need to be changed after the original code is written.

Note: There are two extra dummy controls added to the function for demonstration purposes. In your own code these will need to be changed or removed.

 

Step four: give the control focus or the draw cell

Add an OnDrawColumnCell event handler to the grid (do not use OnDrawDataCell; see Listing C). Here is where the grid starts doing its work and it is probably the most confusing part. This function has four parameters and we will use three of them. The first parameter of note is called State. The grid is either going to give the appropriate control focus or is going to redraw the cell depending on the value of the State parameter. The OnDrawColumnCell event handler performs two functions and the FindGridControl() function is used in both cases. The following sections will explain each of these functions.

Handling control focus

If the State parameter is set to gdFocused or gdSelected and FindGridControl() does not return NULL, the database control will have its Visible property set to true and receive focus. If the control found by FindGridControl() is not DBCheckBoxGrid then the grid will resize the control to fit the current cell by using the values in the Rect parameter.

A TDBCheckBox must be centered manually so it’s width and height will stay the same and the grid’s canvas will be erased in the immediate area behind DBCheckBoxGrid. The TDBCheckBox is centered by using values returned by the function NewDimension(). This will give the illusion that the TDBCheckBox is part of the grid.

There are a couple of things to pay attention to here:

 

Drawing the cell

The second part of this function is to draw the correct image in the correct cell. This only occurs if the State variable is not set to gdFocused or gdSelected and FindGridControl() does not return NULL. If all is well, the SetCheck() function is called to draw the check mark in the cell. The value of the Boolean field is determined so the grid knows whether to paint a checked or an unchecked mark. The Rect parameter is passed to the SetCheck() function so that the mark is drawn in the correct location. Since there is no other use for the control at this point, the FindGridControl() function sets its Visible property to false.

 

Step five: handling loss of focus

Add an OnColExit event handler for the grid. The code for the event handler is shown in Listing E. As you can see, the event handler calls FindGridControl() function. It passes the grid’s SelectedField->FieldName value for the first parameter, and false for the second parameter. If FindGridControl() returns a non-NULL value it means there is a control that needs to be hidden. Nothing is done with the return value so it is not used.

 

Step six: handling key press events

This is the last step. Create an OnKeyPress event handler for the grid and enter the code shown in Listing F. This event handler also calls the FindGridControl() function. If FindGridControl() returns a non-NULL values, the resulting pointer’s Visible property is set to true. The remaining code forces the grid to repaint and then gives the appropriate control focus. As a last step, all keystrokes are sent to the control using the SendMessage() function.

 

Conclusion

If used judiciously, these relatively simple steps of adding controls to a grid can benefit the end user by making the application more user-friendly. There are some issues that are not addressed here, however. For example, there is nothing to keep the user from clicking “behind” the checkbox (the normal “True” or “False” will appear). Also, the FindGridControl() function would be much nicer if the controls had pointers stored in a TList instead of a static TWinControl array. Still, you now have the basics for displaying other controls on a TDBGrid.

 

Acknowledgment: This article was based on an article and code originally written by Alec Bergamini (O&A Productions www.o2a.com) for Delphi 1.0 taken from a CD called Delphi Developers Kit. Used by permission

 

Listing A

void TForm1::SetCheck(
  const TRect &Rect, bool IsChecked)
{
  // Code to create checked
  // or unchecked mark
  DBGrid1->Canvas->FillRect(Rect);
  int SquareSize = 13;
  // Location of check mark
  int NewTop = NewDimensions(
    Rect.Top, Rect.Bottom, SquareSize);
  int NewLeft = NewDimensions(
    Rect.Left, Rect.Right, SquareSize);
  TRect ARect(NewLeft, NewTop,
    NewLeft + SquareSize, NewTop + SquareSize);
  UINT tmpState = DFCS_BUTTONCHECK |
    (IsChecked == true) ? DFCS_CHECKED : 0;
  DrawFrameControl(DBGrid1->Canvas->Handle,
    &ARect, DFC_BUTTON, tmpState);
}

 

Listing B

TWinControl* TForm1::FindGridControl(
  AnsiString FieldName, bool IsVisible)
{
  TWinControl *tmpWC = NULL;
  TWinControl *tmpWCDBControl[] = {DBCheckBoxGrid,
    FirstDBControl, SecondDBControl, NULL};
  AnsiString tmpWCFieldName[3];

  tmpWCFieldName[0] = DBCheckBoxGrid->DataField;
  tmpWCFieldName[1] = FirstDBControl->DataField;
  tmpWCFieldName[2] = SecondDBControl->DataField;

  int n = 0;
  while (tmpWCDBControl[n] != NULL)
  {
    if (FieldName == tmpWCFieldName[n])
    {
      tmpWC = tmpWCDBControl[n];
      tmpWC->Visible = IsVisible;
      if (tmpWC->Parent != DBGrid1->Parent)
        tmpWC->Parent = DBGrid1->Parent;
      break;
    }
  ++n;
  }
  return tmpWC;
}

 

Listing C

 

void __fastcall TForm1::DBGrid1DrawColumnCell(
  TObject *Sender, const TRect &Rect, int DataCol,
  TColumn *Column, TGridDrawState State)
{
  if (State.Contains(gdFocused) ||
      State.Contains(gdSelected))
  {
    TWinControl *tmpWC =
      FindGridControl(Column->FieldName, true);
    if (tmpWC != NULL)
    {
      if (tmpWC == DBCheckBoxGrid)
      {
        int BorderMargin =
          DBGrid1->BorderStyle == bsSingle ? 2 : 0;

          DBGrid1->Canvas->FillRect(Rect);
          //in case it is reused for multiple fields
          DBCheckBoxGrid->DataField =
            Column->FieldName;
          int NewLeft = NewDimensions(Rect.Left,
            Rect.Right, DBCheckBoxGrid->Width);
          tmpWC->Left = NewLeft +
            DBGrid1->Left + BorderMargin;
          int NewTop = NewDimensions(Rect.Top,
            Rect.Bottom, DBCheckBoxGrid->Height);
          tmpWC->Top = NewTop +
            DBGrid1->Top + BorderMargin;
      }
      else
      {
        int LineMargin = 1;
        tmpWC->Left = Rect.Left + LineMargin;
        tmpWC->Width =
          Rect.Right - Rect.Left - LineMargin;
        tmpWC->Top =
          Rect.Top + LineMargin + DBGrid1->Top;
        tmpWC->Height =
          Rect.Bottom - Rect.Top - LineMargin;
      }
    }
  }
  else
  {
    if (FindGridControl(Column->FieldName,
          false) == DBCheckBoxGrid)
    {
      bool IsChecked = Table1->FieldByName(
        Column->FieldName)->AsBoolean;
      SetCheck(Rect, IsChecked);
    }
  }
}

 

Listing D

void __fastcall TForm1::DBGrid1ColExit(
  TObject *Sender)
{
  FindGridControl(
    DBGrid1->SelectedField->FieldName, false);
}

 

Listing E

void __fastcall TForm1::DBGrid1KeyPress(
  TObject *Sender, char &Key)
{
  TWinControl *tmpWC = FindGridControl(
    DBGrid1->SelectedField->FieldName, true);
  if (tmpWC != NULL)
  {
    DBGrid1->Repaint();
    tmpWC->SetFocus();
    SendMessage(tmpWC->Handle, WM_CHAR, Key, 0);
  }
}


Listing F

int TForm1::NewDimensions(
  int Rect1, int Rect2, int Dimension)
{
  int NewDm = Rect2 - Rect1 - Dimension;
  NewDm = Rect1 + ((NewDm > 0) ? NewDm / 2 : 0);
  return NewDm;
}