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

Aide-mémoire : DevExpress cxGrid et Delphi 7

Lorsqu’on récupère un grand nombre d’enregistrements, une erreur de mémoire insuffisante peut survenir, alors que cxGrid est capable d’en traiter plusieurs fois davantage, même dans une application 32 bits. Remplacer le gestionnaire de mémoire standard de Delphi par Fast Memory Manager peut aider : dans mon cas, FastMM4 a multiplié par cinq le nombre maximal d’enregistrements que je pouvais récupérer.

Voici quelques techniques pour utiliser cxGrid, recueillies auprès de différentes sources et qui m’ont été utiles dans mes projets. (Mis à jour le 6 octobre 2026)

 

Apparence

On peut définir les couleurs des cellules par programmation dans l’événement OnCustomDrawCell :

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

Pour colorer seulement certaines colonnes plutôt que toute la ligne, on peut identifier la colonne dans le gestionnaire OnCustomDrawCell ainsi :

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

Attention : les couleurs appliquées par dessin personnalisé ne sont pas conservées lors de l’export vers Excel, et la mise en évidence de la sélection peut également poser problème. Pour conserver la mise en forme, affectez un style prédéfini dans un gestionnaire tel que TableViewStylesGet***Style :

procedure TMoneyMoveDetailForm.TableViewStylesGetContentStyle(Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord; AItem: TcxCustomGridTableItem; out AStyle: TcxStyle);
begin
	if not ARecord.IsData then Exit;
	// (AItem as TcxGridDBBandedColumn) pour identifier la colonne courante
	if (ARecord.Values[TableViewID_MONEY.Index])=null then begin
		AStyle :=cxStyleBold;
	end;
end;

Afficher une image ou une icône dans une cellule

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;

Autre possibilité :

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;

On peut aussi choisir ButtonEdit dans les Properties de la cellule et ajouter un ou plusieurs boutons. Affecter une Action à un bouton le rend fonctionnel, à condition que l’édition soit activée. L’inconvénient est que la cellule reste un éditeur de texte.

Utiliser des couleurs différentes selon le niveau de regroupement

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;

Un inconvénient des styles est que la mise en évidence de la cellule active peut masquer les couleurs. Pour contourner cela, on peut dessiner soi-même la sélection de la ligne dans TableViewCustomDrawCell, par exemple avec CellSelect=True :

  if AViewInfo.GridRecord.Selected then begin
    if AViewInfo.Selected then begin // texte blanc dans la cellule active
      ACanvas.Font.Color := clWhite;
    end else begin // texte bleu dans le reste de la ligne, sans masquer les fonds
      ACanvas.Font.Color := clBlue;
      ACanvas.Font.Style := [fsBold];
    end;
  end;

Attention : accéder aux données d’une ligne par GridRecord et l’indice de colonne ne donne pas toujours les valeurs attendues lorsque les lignes sont regroupées : la ligne dessinée peut être un en-tête de groupe.

Affichage des données : filtres, regroupement et tri

Pour masquer le GroupByBox de TcxGrid, c’est-à-dire la zone au-dessus de la grille portant le texte « drag a column header here to group by… », utilisez :

TableView.OptionsView.GroupByBox := false;

Définir un filtre par programmation

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

Si vous ajoutez plusieurs conditions, précisez l’opérateur logique avant d’ajouter la suivante. Par défaut, il s’agit de AND ; pour utiliser OR, définissez TableView.DataController.Filter.Root.BoolOperatorKind := fboOr;

Conserver la position dans la grille après actualisation

TableView.BeginUpdate();
r := TableView.Controller.TopRowIndex;
f := TableView.Controller.FocusedRowIndex;
GridQuery.Close; // fermer puis rouvrir le jeu de données
GridQuery.Open;
TableView.Controller.TopRowIndex := r;
TableView.Controller.FocusedRowIndex := f;
TableView.EndUpdate;

Développer ou réduire les groupes par programmation

TableView.DataController.Groups.FullCollapse; // Réduire
TableView.DataController.Groups.FullExpand; // Développer
TableView.DataController.Options := TableView.DataController.Options + [dcoGroupsAlwaysExpanded]; // Maintenir les groupes développés

Ajouter ou supprimer un regroupement par programmation

TableViewColumn1.GroupIndex := 1; // Regrouper selon cette colonne
TableView.DataController.Groups.ClearGrouping; // Supprimer le regroupement

Accéder aux données et métadonnées de la grille

Parcourir toutes les lignes visibles

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

Parcourir les lignes sélectionnées et récupérer l’identifiant de chaque enregistrement

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

Pour obtenir une colonne de la grille à partir du nom de son champ :

TableView1.GetColumnByFieldName('FIELD_NAME')

Dans TcxGridTableView et TcxGridBandedTableView, sans le préfixe « DB », on peut ajouter et supprimer les données manuellement. Par exemple :

// Supprimer toutes les lignes
while TableView.DataController.RowCount>0 do
  TableView.DataController.DeleteRecord(0);

// Ajouter une ligne et affecter des valeurs selon leur 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);

Modifier les données dans la grille

Remplir un ComboBox dans une cellule

Choisissez ComboBox dans Properties et remplissez sa liste dans l’événement AfterScroll du jeu de données. Lorsque j’essayais de le faire dans onInitPopUp, c’était la liste précédente qui apparaissait. Des lecteurs m’ont signalé dans les commentaires que je m’y prenais mal. Voici un exemple pour remplir la liste :

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

Vérifiez et traitez le choix de l’utilisateur dans OnValidate, par exemple :

if LookupQuery.Locate('CNAME',DisplayValue,[]) then begin
	// enregistrer dans la base de données
	TableView.BeginUpdate;
	// actualiser la grille si nécessaire
	TableView.EndUpdate;
end else begin
	ErrorText := 'Sélection incorrecte';
	Error := True;
end;
TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;

Modifier un nombre dans la grille

Choisissez CalcEdit dans Properties. Dans le gestionnaire OnEditValueChanged, utilisez (Sender as TcxCalcEdit).Value et l’enregistrement courant du jeu de données.

Détecter un clic droit sur une colonne donnée

Dans l’événement OnCellClick :

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

Copier le texte affiché d’une cellule dans le presse-papiers

La valeur de la cellule courante est accessible avec <TcxGridDBTableView>.Controller.FocusedRow.Values[FocusedColumn.Index]. Mais pour une colonne utilisant LookupComboBox, cette expression renvoie la clé et non le texte affiché. La fonction de copie doit en tenir compte et appeler la méthode masquée GetDisplayLookupText. Pour y accéder, déclarez une classe dérivée de TcxLookupComboBoxProperties :

type
  TcxLookupComboBoxPropertiesAccess = class(TcxLookupComboBoxProperties); // pour accéder à .GetDisplayLookupText

// plus loin dans le 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;

Identifier l’élément sous le pointeur dans 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); // Types possibles : // TcxGridColumnHeaderHitTest, TcxGridRecordCellHitTest, TcxGridGroupByBoxHitTest
  if AHitTest is TcxGridColumnHeaderHitTest then begin
    if TcxGridColumnHeaderHitTest(AHitTest).Column = TableView1COLUMN1 then begin
      // Clic sur le premier en-tête de colonne ; éviter de modifier le tri ici
    end;
  end;
end;

Pour traiter le changement d’une case à cocher dans une cellule, utilisez l’événement OnChange :

if (Sender as TcxCheckBox).Checked then

Identifier la colonne en cours de modification

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

Modifier d’autres cellules dans OnEditValueChanged

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

Pour une raison qui m’échappe, cette méthode ne fonctionne pas toujours chez moi. On peut aussi procéder ainsi :

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

Glisser-déposer sur les lignes de la grille

procedure TForm1.cxGrid2DBTableView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Démarrer le glissement depuis la grille source (on peut aussi activer 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
// Obtenir ARecordIndex de l'enregistrement déplacé
  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
// Autoriser le dépôt

    Accept := Source is TcxDragControlObject;
end;

procedure TForm1.cxGrid2DBTableView1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
// Afficher le texte de la ligne de destination
    self.Caption := TcxGridRecordCellHitTest(TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y)).GridRecord.Values[cxGrid1DBTableView1FULLNAME.Index];
end;

Modifier la grille à l’exécution

Ajouter une colonne à l’exécution :

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

Supprimer une colonne à l’exécution

TableView.Columns[i].Destroy;

Ajouter des bandes :

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

Définir une formule personnalisée pour FooterSummary

Pour calculer un résumé de pied de grille à partir des valeurs d’autres résumés, utilisez DataControllerSummaryAfterSummary. Cela ne fonctionne pas dans l’événement OnGetText de la cellule, car les résumés ne sont pas encore calculés à ce moment-là.

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;

Pour GroupSummary, il faut parcourir récursivement tous les niveaux de regroupement et calculer la formule souhaitée pour les résumés de chaque groupe. Voir cet exemple d’implémentation.

Bugs et solutions de contournement

Avec un composant aussi complexe, on finit parfois par rencontrer des bugs. Dans certains cas, les totaux du pied de grille perdaient leurs chiffres de tête : par exemple, 14.8 s’affichait sous la forme 4.8, quel que soit le format choisi. J’avais contourné le problème en calculant moi-même la somme dans l’événement OnGetText du résumé de pied :

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;

C’était mon ancien contournement. En 2026, j’ai enfin trouvé la cause : un bug dans la bibliothèque RTL standard de Delphi 7 ! Je l’explique dans cet article, disponible en russe.

TcxLookupComboBox

Afficher une indication dans le champ de saisie avec TextHint. Le composant possède cette propriété, mais elle est masquée. Pour la rendre accessible, déclarez une classe dérivée :

type
  TcxLookupComboBoxAccess = class(TcxLookupComboBox); // pour accéder à la propriété masquée .TextHint

// Affecter ensuite le texte indicatif, par exemple dans OnCreate :

TcxLookupComboBoxAccess(MyLookupCoboBox).TextHint := 'Texte indicatif';

Ajouter votre propre bouton au champ de saisie, par exemple pour ouvrir ou modifier une liste de référence. Cela fonctionne également avec d’autres contrôles possédant des Properties ; voir la documentation des contrôles.

with MycxLookupComboBox.Properties do begin
    Images := cxImageList;
    Result := Buttons.Add;
    Result.Default := True;
    Result.Kind := bkGlyph;
    Result.LeftAlignment := False;
    Result.Action := Action; // L'action à exécuter
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: Спасибо за замечание!

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *