Cheat sheet: DevExpress cxGrid and Delphi 7

When retrieving a large number of records you may encounter an out-of-memory error, even though cxGrid itself handles several times more records even in 32-bit mode. Simply replace the standard Delphi memory manager with “Fast Memory Manager” — in my case FastMM4 worked perfectly, increasing the maximum fetch size by 5×.

Some techniques for working with the cxGrid component, collected from various sources and useful to me personally. (Updated 12.12.2022)

Appearance

Programmatic cell coloring can be done in the OnCustomDrawCell event:

if (AViewInfo.GridRecord.Values[TableViewColumn1.Index])=1 then begin
    ACanvas.Brush.Color := clRed;
    ACanvas.Font.Style := [fsBold];
end;

If you want to color only specific columns (rather than the entire row), you can identify the column being drawn in the OnCustomDrawCell handler like this:

if TableView.Columns[AViewInfo.Item.Index].DataBinding.FieldName='COLUMN_1' then
or
if AViewInfo.Item = TableViewCOLUMN_1 then

Note that this type of coloring is not rendered when exporting to Excel, and the selection highlight color may also be an issue. To work around this, use styles assigned in handlers of the “TableViewStylesGet***Style” type instead:

procedure TMoneyMoveDetailForm.TableViewStylesGetContentStyle(Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord; AItem: TcxCustomGridTableItem; out AStyle: TcxStyle);
begin
    if not ARecord.IsData then Exit;
    // (AItem as TcxGridDBBandedColumn) to check which column we are in
    if (ARecord.Values[TableViewID_MONEY.Index])=null then begin
        AStyle :=cxStyleBold;
    end;
end;

Displaying an image or icon in a cell

procedure TMsgForm.TableViewIMAGECustomDrawCell(Sender: TcxCustomGridTableView; ACanvas: TcxCanvas; AViewInfo: TcxGridTableDataCellViewInfo; var ADone: Boolean);
Var
    r : TRect;
    iImageIndex : Integer;
begin
    If (AViewInfo.GridRecord.Values[TableViewIMAGE.Index] = 1) then begin
        R := AViewInfo.Bounds;
        ACanvas.Brush.Color := AViewInfo.Params.Color;
        ACanvas.FillRect(R);

        // draw the image
        R := AViewInfo.Bounds;
        Inc(r.Top,1);
        ACanvas.DrawImage(ImageList,r.Left,r.Top,20,True);
        ADone := True ;
    end;
end;

Or alternatively:

procedure TFilesForm.TableViewIconCustomDrawCell(
Sender: TcxCustomGridTableView; ACanvas: TcxCanvas;
AViewInfo: TcxGridTableDataCellViewInfo; var ADone: Boolean);
begin
    inherited;
    with AViewInfo.ClientBounds do
        cxImageList.Draw(ACanvas.Canvas, Left + 1, Top + 1, 0);
    ADone := True;
end;

Another option is to set a ButtonEdit component in the cell’s Properties, where you can add one or more buttons. If you attach an Action to the button it also becomes clickable — but only when editing is enabled. The downside is that the cell then displays text only.

Coloring different grouping levels with different colors:

procedure TPozSkladForm.TableViewStylesGetGroupStyle( Sender: TcxGridTableView; ARecord: TcxCustomGridRecord; ALevel: Integer; out AStyle: TcxStyle);
begin
    if ALevel=0 then AStyle := cxStyleL0;  // $00A0A0A0
    if ALevel=1 then AStyle := cxStyleL1;  // clSilver
    if ALevel=2 then AStyle := cxStyleL2;  // $00E0E0E0
end;

A notable downside of style-based coloring is that the cursor highlight covers the color and it becomes invisible. You can work around this by handling the highlight yourself in the TableViewCustomDrawCell handler (for CellSelect=True mode):

  if AViewInfo.GridRecord.Selected then begin
    if AViewInfo.Selected then begin // under the inverted cursor — white font
      ACanvas.Font.Color := clWhite;
    end else begin // rest of the row — blue bold font, so background coloring is preserved
      ACanvas.Font.Color := clBlue;
      ACanvas.Font.Style := [fsBold];
    end;
  end;

Note! When accessing row data via GridRecord and a column index, you may not get the expected values in case of row grouping, when the rendered row is actually a group header row.

Displaying data (filters, grouping, sorting)

In TcxGrid, to hide the GroupByBox and remove the area above the grid that says “drag a column header here to group by ..”:

TableView.OptionsView.GroupByBox := false;

Setting a filter programmatically

TableView.DataController.Filter.BeginUpdate;
TableView.DataController.Filter.Root.Clear;
TableView.DataController.Filter.Root.AddItem(TableViewColumn1, cxFilter.foLike, BegString+"%", BegString+"%");
TableView.DataController.Filter.Active:=true;
TableView.DataController.Filter.EndUpdate

When adding more than one condition, specify the boolean operator type before adding the next one (AND by default), or use OR — TableView.DataController.Filter.Root.BoolOperatorKind := fboOr;

Saving the cursor position in the grid after a refresh

TableView.BeginUpdate();
r := TableView.Controller.TopRowIndex;
f := TableView.Controller.FocusedRowIndex;
GridQuery.Close; // close and reopen the grid here
GridQuery.Open;
TableView.Controller.TopRowIndex := r;
TableView.Controller.FocusedRowIndex := f;
TableView.EndUpdate;

Expanding or collapsing groups programmatically

TableView.DataController.Groups.FullCollapse; // Collapse
TableView.DataController.Groups.FullExpand; // Expand
TableView.DataController.Options := TableView.DataController.Options + [dcoGroupsAlwaysExpanded]; // Lock expanded state

Setting or clearing grouping programmatically

TableViewColumn1.GroupIndex := 1; // group by this column
TableView.DataController.Groups.ClearGrouping; // remove grouping

Accessing grid data and metadata

Loop through all visible rows:

for I:=0 to cxGrid.DataController.FilteredRecordCount - 1 do begin
cxGrid.DataController.Values[cxGrid.DataController.FilteredRecordIndex[i],YourColumnName.Index])

Loop through all selected rows and get each row’s identifier

for i := 0 to GridView1.Controller.SelectedRecordCount-1 do begin
ID_RASHODD := GridView1.DataController.Values[GridView1.Controller.SelectedRecords[i].RecordIndex, GridView1ID_RASHODD.Index];
end;

Get a grid column by its field name:

TableView1.GetColumnByFieldName('FIELD_NAME')

For TcxGridTableView or TcxGridBandedTableView tables (without “DB”), data can be added and deleted manually:

// Delete all rows from the table
while TableView.DataController.RowCount>0 do
  TableView.DataController.DeleteRecord(0);

// Add a row and fill cells with values according to their type
r := TableView.DataController.AppendRecord;
TableView.DataController.SetValue(r, TableViewColumn1.Index, 'test1');
TableView.DataController.SetValue(r, TableViewColumn2.Index, 'test2');
TableView.DataController.SetValue(r, TableViewColumn3.Index, 12345);

User data editing

Setting up a ComboBox list in a cell:

In Properties set ComboBox, and fill the list in the dataset’s AfterScroll event. If you try to do it in onInitPopUp, the previous list is shown (as pointed out in comments — that approach is wrong). Example:

TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;
while not LookupQuery.Eof do begin
    TcxComboBoxProperties(TableViewCNAME.Properties).Items.Add(LookupQuery['CNAME']);
    LookupQuery.Next;
end;

The user’s selection is validated and used in the OnValidate handler roughly like this:

if LookupQuery.Locate('CNAME',DisplayValue,[]) then begin
    // save to the database here
    TableView.BeginUpdate;
    // refresh the table if needed
    TableView.EndUpdate;
end else begin
    ErrorText := 'Selection error';
    Error := True;
end;
TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;

Editing a number in the grid:

Set CalcEdit in Properties and in the OnEditValueChanged handler use (Sender as TcxCalcEdit).Value together with the current dataset position.

Catching a right-click on a specific column:

In the OnCellClick event:

if ACellViewInfo.Item.Name='DzTableViewNRM_NAME' then
if AButton=mbRight then

Implementing copy-cell-to-clipboard

You can get the value of the focused cell via <TcxGridDBTableView>.Controller.FocusedRow.Values[FocusedColumn.Index] — but if the table has LookupComboBox columns, you will get the key instead of the displayed text. The function must account for this and use .GetDisplayLookupText, which is protected. To access it, inherit the class in your declarations:

type
  TcxLookupComboBoxPropertiesAccess = class(TcxLookupComboBoxProperties); // to access .GetDisplayLookupText

// then in code:

with <TcxGridDBTableView>.Controller do begin
  if FocusedColumn.Properties is TcxLookupComboboxProperties then begin
    Clipboard.AsText := (TcxLookupComboBoxPropertiesAccess(FocusedColumn.Properties).GetDisplayLookupText(FocusedRow.Values[FocusedColumn.Index]));
  end else begin
    Clipboard.AsText := FocusedRow.Values[FocusedColumn.Index];
  end;
end;

Determining the cursor position in MouseUp/MouseDown:

procedure TPlanForm.TableView1MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  AHitTest: TcxCustomGridHitTest;
begin
  inherited;
  AHitTest := (Sender as TcxGridSite).GridView.ViewInfo.GetHitTest(X,Y); // Can be TcxGridColumnHeaderHitTest, TcxGridRecordCellHitTest, TcxGridGroupByBoxHitTest
  if AHitTest is TcxGridColumnHeaderHitTest then begin
    if TcxGridColumnHeaderHitTest(AHitTest).Column = TableView1COLUMN1 then begin
      // Detected a click on the first column header (not a good place to set sorting though)
    end;
  end;
end;

Handling a checkbox toggle in a cell. In the OnChange event:

if (Sender as TcxCheckBox).Checked then

Finding out which column is currently being edited:

TableView.VisibleColumns[TableView.Controller.FocusedColumnIndex].DataBinding.FieldName

Modifying adjacent cells while handling OnEditValueChanged:

TableView.DataController.SetEditValue(TableViewFIELD_NAME.Index, new_Value, evsValue );

This method does not always work for some reason, so alternatively:

TableView1.DataController.DataSource.DataSet.Edit;
TableView1.DataController.DataSource.DataSet.FieldByName('FIELD_NAME').Value := new_Value;
TableView1.DataController.DataSource.DataSet.Post;

Using drag & drop onto grid rows

procedure TForm1.cxGrid2DBTableView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Start dragging from the source table (or simply enable Auto)
    if ((Button = mbLeft) and (ssAlt in Shift)) or (ssRight in Shift) then
        if (TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y).HitTestCode in [htCell, htRecord])
            then TcxGridSite(Sender).BeginDrag(False);
end;

var
  ARecordIndex: Integer;
procedure TForm1.cxGrid2DBTableView1StartDrag(Sender: TObject; var DragObject: TDragObject);
var
  AGridView: TcxGridDBTableView;
begin
// Get ARecordIndex of the record being dragged
  AGridView := TcxGridSite(Sender).GridView as TcxGridDBTableView;
  ARecordIndex := AGridView.Controller.FocusedRecordIndex;
end;

procedure TForm1.cxGrid2DBTableView1DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
// Accept the drop
    Accept := Source is TcxDragControlObject;
end;

procedure TForm1.cxGrid2DBTableView1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
// Show the text from the row where the drop occurred
    self.Caption := TcxGridRecordCellHitTest(TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y)).GridRecord.Values[cxGrid1DBTableView1FULLNAME.Index];
end;

Modifying the table at runtime

Adding a column at runtime:

var
    XCol : TcxGridDBBandedColumn;
begin
    XCol := TableView.CreateColumn;
    XCol.Position.BandIndex := 1;
    XCol.DataBinding.FieldName:='quant'+sl[i];
    XCol.DataBinding.ValueType := 'Float';
    XCol.Caption := ss[i];
    XCol.Width := 50;
    XCol.Tag := StrToInt(sl[i]);
    XCol.Summary.FooterKind := skSum;
    XCol.Summary.GroupFooterKind := skSum;
    XCol.Summary.GroupKind := skSum;
    XCol.Summary.FooterFormat := '0.##';
    XCol.Summary.GroupFooterFormat := '0.##';
    XCol.Summary.GroupFormat := '0.##';

Removing a column at runtime:

TableView.Columns[i].Destroy;

Adding bands:

b0 := TableView.Bands.Add;
b0.Position.BandIndex := b.Index;

Setting a custom formula for FooterSummary

A custom FooterSummary calculation using values from other FooterSummary cells can be done in the DataControllerSummaryAfterSummary event. This cannot be done in the cell’s OnGetText event because Summary values are not yet calculated at that point.

procedure T*****Form.TableViewDataControllerSummaryAfterSummary(ASender: TcxDataSummary);
var
  itog_summa, kmsumma :Variant;
begin
  inherited;
  itog_summa := ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewITOG_SUMMA)];
  kmsumma := ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewKMSUMMA)];
  ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewPERCENT)] := itog_summa + kmsumma;
end;

For GroupSummary, you will need to recursively iterate over all levels and recalculate the formula for each GroupSummary at every level. Implementation example.

Bugs

A complex component like this inevitably has bugs, and you may encounter them occasionally. For example, in some cases the Footer would lose significant digits during summation — displaying 4.8 instead of 14.8, regardless of the defined format. In those cases I had to write a custom summation routine and call it in the OnGetText event on FooterSummary:

procedure T****Form.TableViewTcxGridDBDataControllerTcxDataSummaryFooterSummaryItems3GetText(
Sender: TcxDataSummaryItem; const AValue: Variant; AIsFooter: Boolean;
var AText: String);
  var
    s : Double;
    i : Integer;
begin
  s := 0;
  for i:=0 to TableView.DataController.FilteredRecordCount - 1 do begin
    try
    s := s + TableView.DataController.GetValue(TableView.DataController.FilteredRecordIndex[i], Sender.Field.Index);
    except
    end
  end;
  AText := FormatFloat(Sender.Format, s);
end;

TcxLookupComboBox

Showing a placeholder hint in the input field (.TextHint) — This property exists on the component but is protected. To access it, inherit the class in your declarations:

type
  TcxLookupComboBoxAccess = class(TcxLookupComboBox); // to access the hidden .TextHint field

// Then in OnCreate assign the hint:

TcxLookupComboBoxAccess(MyLookupCoboBox).TextHint := 'Hint text here';

Adding a custom extra button to the input field, for example to open a lookup or editor. This also works for all controls that have Properties (list of controls here):

with MycxLookupComboBox.Properties do begin
    Images := cxImageList;
    Result := Buttons.Add;
    Result.Default := True;
    Result.Kind := bkGlyph;
    Result.LeftAlignment := False;
    Result.Action := Action; // The action to be executed
end;

Leave a Comment

Your email address will not be published. Required fields are marked *