Без рубрики @en

Cheat sheet: DevExpress cxGrid and Delphi 7

When fetching a large number of records, you may run into an out-of-memory error, even though cxGrid itself can handle several times as many records, including in a 32-bit application. Replacing Delphi’s standard memory manager with Fast Memory Manager can help: in my case, FastMM4 increased the maximum number of records I could fetch by a factor of five.

Here are some cxGrid techniques collected from various sources that I have found useful in my own work. (Updated October 6, 2026)

 

Appearance

You can set cell colors programmatically in the OnCustomDrawCell event:

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

To color only specific columns rather than the entire row, identify the column in the OnCustomDrawCell handler like this:

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

Bear in mind that custom-drawn colors are not included in an Excel export, and selection highlighting can also cause problems. To preserve the formatting, assign a predefined style in a handler such as TableViewStylesGet***Style:

procedure TMoneyMoveDetailForm.TableViewStylesGetContentStyle(Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord; AItem: TcxCustomGridTableItem; out AStyle: TcxStyle);
begin
	if not ARecord.IsData then Exit;
	// (AItem as TcxGridDBBandedColumn) to identify the current column
	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;

Another option is:

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;

You can also choose ButtonEdit in the cell’s Properties and add one or more buttons. Assigning an Action to a button makes it functional, provided editing is enabled. The drawback is that the cell remains a text editor.

Using different colors for different grouping levels

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;

One drawback of styles is that the focused-cell highlight can obscure the colors. To work around this, draw the row selection yourself in TableViewCustomDrawCell, for example with CellSelect=True:

  if AViewInfo.GridRecord.Selected then begin
    if AViewInfo.Selected then begin // white text for the focused cell
      ACanvas.Font.Color := clWhite;
    end else begin // blue text for the rest of the row, preserving cell backgrounds
      ACanvas.Font.Color := clBlue;
      ACanvas.Font.Style := [fsBold];
    end;
  end;

Watch out: accessing row data through GridRecord and a column index may not return what you expect when rows are grouped: the row being drawn may be a group header.

Displaying data: filtering, grouping and sorting

To hide the GroupByBox in TcxGrid—the area above the grid that says “drag a column header here to group by…”—use:

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 before adding the next one. The default is AND; to use OR, set TableView.DataController.Filter.Root.BoolOperatorKind := fboOr;

Preserving the grid position after refreshing data

TableView.BeginUpdate();
r := TableView.Controller.TopRowIndex;
f := TableView.Controller.FocusedRowIndex;
GridQuery.Close; // close and reopen the dataset
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]; // Keep groups expanded

Adding or removing grouping programmatically

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

Accessing grid data and metadata

Iterating over all visible rows

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

Iterating over selected rows and retrieving each record’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;

To find a grid column by its field name:

TableView1.GetColumnByFieldName('FIELD_NAME')

For TcxGridTableView and TcxGridBandedTableView without the “DB” prefix, you can add and remove data manually. For example:

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

// Add a row and set values using the appropriate types
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);

Editing data in the grid

Populating a ComboBox in a cell

Choose ComboBox in Properties and populate its list in the dataset’s AfterScroll event. When I tried doing this in onInitPopUp, the previous list would appear. Readers pointed out in the comments that I was doing it incorrectly. Here is an example of populating the list:

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

Check and process the user’s selection in OnValidate, for example:

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

Editing a number in the grid

Choose CalcEdit in Properties. In the OnEditValueChanged handler, use (Sender as TcxCalcEdit).Value and the dataset’s current record.

Detecting a right-click on a particular column

In the OnCellClick event:

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

Copying a cell’s displayed value to the clipboard

You can read the current cell with <TcxGridDBTableView>.Controller.FocusedRow.Values[FocusedColumn.Index]. However, for a column using LookupComboBox, this returns the key rather than the displayed text. The copying routine needs to account for that and call the hidden GetDisplayLookupText method. To access it, declare a descendant of TcxLookupComboBoxProperties:

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

// later in the 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;

Finding what is under the pointer 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); // Possible types: // TcxGridColumnHeaderHitTest, TcxGridRecordCellHitTest, TcxGridGroupByBoxHitTest
  if AHitTest is TcxGridColumnHeaderHitTest then begin
    if TcxGridColumnHeaderHitTest(AHitTest).Column = TableView1COLUMN1 then begin
      // First column header clicked; setting sorting here is a bad idea
    end;
  end;
end;

To process a checkbox change in a cell, use the OnChange event:

if (Sender as TcxCheckBox).Checked then

Finding the column currently being edited

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

Updating other cells in OnEditValueChanged

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

For some reason this approach does not always work for me. Here is another way:

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

Dragging and dropping onto grid rows

procedure TForm1.cxGrid2DBTableView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Start dragging from the source grid (Auto is another option)
    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 dragged record
  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
// Allow the drop

    Accept := Source is TcxDragControlObject;
end;

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

Changing the grid 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.##';

Deleting a column at runtime

TableView.Columns[i].Destroy;

Adding bands:

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

Using a custom formula for FooterSummary

To calculate a footer summary using values from other footer summaries, use DataControllerSummaryAfterSummary. Doing this in the cell’s OnGetText event will not work because the summaries have not yet been calculated when that event runs.

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 need to walk through all grouping levels recursively and calculate the required formula for each group’s summaries. See this implementation example.

Bugs and workarounds

With a component this complex, you will occasionally run into bugs. In some cases, the footer totals lost their leading digits: for example, 14.8 appeared as 4.8, regardless of the specified format. I worked around this by calculating the total myself in the footer summary’s OnGetText event:

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;

That was my old workaround. In 2026 I finally tracked down the cause: a bug in Delphi 7’s standard RTL! I describe it in this article.

TcxLookupComboBox

Showing a hint inside the input field with TextHint. The component has this property, but it is hidden. To expose it, declare a descendant class:

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

// Then assign the hint, for example in OnCreate:

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

Adding your own button to the input field, for example to open or edit a lookup list. This also works for other controls that have Properties; see the control documentation.

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 execute
end;

Discussion 4

  1. Добрый день!
    Не подскажите, как можно сохранить настройки и расположение колонок в реестре или ini? В dxMasterView это было из коробки, а вот в cxGrid я не нашел такого способа. Смысл, чтобы сохранить стили, размеры и порядок колонок в cxGridTableView.

    1. Юзайте TcxPropertiesStore, но в гридах вьюшку надо заменить на Banded , т.е. вместо например TcxDBGridTableView , надо заменить на TCxDbBandedTableView. Это надо что бы было доступно свойство position (т.е. расположение столбца в гриде)

  2. Отличная статья! У меня задача: заменить значения чисел на текстовые значения. Спасибо.

  3. Спасибо вам за статью.
    Но есть замечание относительно пункта “Редактирование данных пользователем”, подпункт “Организация списка ComboBox в ячейке”. На самом деле в OnInitPopup тоже можно сделать выпадающий список и он будет актуальным, нужно просто обращаться не к столбцу, а к Sender’-у. Что-то типа этого:

    procedure TrdbXXX.YYYPropertiesInitPopup(
      Sender: TObject);
    begin
      if XQuery.Active then
      begin
        (Sender as TcxComboBox).Properties.Items.Clear;
        XQuery.First;
        while notXQuery.Eof do
        begin
          (Sender as TcxComboBox).Properties.Items.Add(XQuery['FieldName']);
          XQuery.Next;
        end;
      end;
    end;

    a7in: Спасибо за замечание!

Leave a Reply

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