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;
