Aide-mémoire DevExpress cxGrid et Delphi 7

Lors de la récupération d’un grand nombre d’enregistrements, vous pouvez rencontrer une erreur de mémoire insuffisante, bien que cxGrid lui-même gère facilement plusieurs fois plus d’enregistrements même en version 32 bits. Il suffit de remplacer le gestionnaire de mémoire standard de Delphi par « Fast Memory Manager » — dans mon cas, FastMM4 a parfaitement aidé à étendre les capacités en multipliant par 5 la limite de sélection.

Quelques techniques de travail avec le composant cxGrid, collectées de diverses sources et utiles personnellement. (Mis à jour le 12.12.2022)

Apparence visuelle

La coloration programmée des cellules peut être effectuée dans les événements OnCustomDrawCell

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

Si vous souhaitez colorier uniquement certaines colonnes (et non toute la ligne), la colonne à colorier dans le gestionnaire OnCustomDrawCell peut être identifiée ainsi :

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

Il faut noter que ce type de coloration n’est pas rendu lors de l’export vers Excel et que la couleur de sélection peut parfois être mal choisie. Pour éviter cela, il vaut mieux colorier exclusivement en assignant un « style » prédéfini dans les gestionnaires de type « TableViewStylesGet***Style » :

procedure TMoneyMoveDetailForm.TableViewStylesGetContentStyle(Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord; AItem: TcxCustomGridTableItem; out AStyle: TcxStyle);
begin
	if not ARecord.IsData then Exit;
	// (AItem as TcxGridDBBandedColumn) si vous avez besoin de savoir dans quelle colonne vous vous trouvez
	if (ARecord.Values[TableViewID_MONEY.Index])=null then begin
		AStyle :=cxStyleBold;
	end;
end;

Affichage d’une image ou 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);

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

Mais on peut aussi faire comme ceci :

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;

Il existe aussi une autre option — définir dans les Properties de la cellule un composant ButtonEdit, dans lequel vous pouvez ajouter un ou plusieurs boutons. Si vous associez une Action au bouton, il devient cliquable — mais uniquement si l’édition est activée. L’inconvénient de cette méthode est que la cellule n’affiche que du texte.

Coloration des différents niveaux de groupement avec des couleurs différentes :

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 notable de la coloration via les styles (Style) est que le curseur masque la coloration et qu’elle devient invisible. Pour contourner cela, vous pouvez gérer vous-même la mise en surbrillance de la ligne dans le gestionnaire TableViewCustomDrawCell (pour le mode CellSelect=True) :

  if AViewInfo.GridRecord.Selected then begin
    if AViewInfo.Selected then begin // sous le curseur inversé, on met la police en blanc
      ACanvas.Font.Color := clWhite;
    end else begin // le reste de la ligne en police bleue pour ne pas perturber la coloration de fond
      ACanvas.Font.Color := clBlue;
      ACanvas.Font.Style := [fsBold];
    end;
  end;

Attention ! Lors de l’accès aux données d’une ligne via GridRecord et l’index de colonne, vous pouvez ne pas obtenir les valeurs attendues en cas de groupement de lignes, lorsque la ligne rendue est une ligne d’en-tête de groupe.

Affichage des données (filtres, groupements, tris)

Dans TcxGrid, pour masquer le GroupByBox et supprimer la zone au-dessus de la grille qui affiche « drag a column header here to group by.. » :

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 plusieurs conditions sont ajoutées, il est conseillé de spécifier le type booléen avant chaque ajout (AND par défaut), mais vous pouvez aussi faire OR — TableView.DataController.Filter.Root.BoolOperatorKind := fboOr;

Sauvegarder la position du curseur dans la grille après actualisation

TableView.BeginUpdate();
r := TableView.Controller.TopRowIndex;
f := TableView.Controller.FocusedRowIndex;
GridQuery.Close; // ici on ferme/rouvre la grille
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]; // Verrouiller l'état développé

Définir ou supprimer un groupement par programmation

TableViewColumn1.GroupIndex := 1; // définir le groupement par colonne
TableView.DataController.Groups.ClearGrouping; // Supprimer le groupement

Accès aux données et métadonnées de la grille

 Parcourir toutes les lignes visibles en boucle :

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

Boucle sur toutes les lignes sélectionnées avec récupération de l’identifiant de chacune

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

Obtenir une colonne de la grille par son nom

TableView1.GetColumnByFieldName('FIELD_NAME')

Pour les tables de type TcxGridTableView ou TcxGridBandedTableView (sans « DB »), les données peuvent être ajoutées et supprimées manuellement. Cela peut se faire par exemple ainsi :

// Suppression de toutes les lignes de la table
while TableView.DataController.RowCount>0 do
  TableView.DataController.DeleteRecord(0);

// Ajout d'une ligne de données et remplissage des cellules avec 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);

Édition des données par l’utilisateur

Organisation d’une liste ComboBox dans une cellule :

Dans Properties, définissez ComboBox et remplissez la liste dans les événements AfterScroll du dataset. Si vous essayez de le faire dans onInitPopUp, la liste affichée sera celle de l’appel précédent (les commentaires m’ont indiqué que je faisais cela incorrectement). Exemple de remplissage :

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

Le choix de l’utilisateur est vérifié et utilisé dans la méthode OnValidate à peu près comme ceci :

if LookupQuery.Locate('CNAME',DisplayValue,[]) then begin
	// ici on enregistre en base
	TableView.BeginUpdate;
	// ici on met à jour la table si nécessaire
	TableView.EndUpdate;
end else begin
	ErrorText := 'Erreur de sélection';
	Error := True;
end;
TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;

Édition d’un nombre dans la grille :

Dans Properties, définissez CalcEdit et dans le gestionnaire OnEditValueChanged utilisez (Sender as TcxCalcEdit).Value avec la position courante dans le dataset.

Détecter un clic droit sur une colonne spécifique :

Dans l’événement OnCellClick :

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

Créer une fonction de copie de cellule dans le presse-papiers

On peut récupérer la valeur de la cellule courante ainsi : := <TcxGridDBTableView>.Controller.FocusedRow.Values[FocusedColumn.Index] — mais si votre table contient des colonnes avec LookupComboBox, vous obtiendrez la clé au lieu de la valeur affichée. La fonction de récupération doit donc en tenir compte et utiliser .GetDisplayLookupText, qui est masquée et nécessite d’hériter la classe TcxLookupComboBoxProperties dans vos déclarations :

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

// puis 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;

Déterminer la position du curseur via l’événement 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); // Peut être l'un des // TcxGridColumnHeaderHitTest, TcxGridRecordCellHitTest, TcxGridGroupByBoxHitTest
  if AHitTest is TcxGridColumnHeaderHitTest then begin
    if TcxGridColumnHeaderHitTest(AHitTest).Column = TableView1COLUMN1 then begin
      // On a déterminé qu'un clic sur l'en-tête de la première colonne a eu lieu (mais placer un tri ici n'est pas une bonne idée)
    end;
  end;
end;

Traitement du cochage d’une case à cocher dans une cellule. Dans l’événement OnChange :

if (Sender as TcxCheckBox).Checked then

Savoir dans quelle colonne l’édition est en cours :

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

Modification des cellules voisines lors du traitement de l’édition dans OnEditValueChanged :

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

Il faut dire que cette méthode ne fonctionne pas toujours pour une raison quelconque, mais on peut aussi procéder autrement :

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

Utilisation du drag&drop pour glisser-déposer sur les lignes de la table

procedure TForm1.cxGrid2DBTableView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Début du glissement depuis la table source (on peut aussi simplement 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
// On récupère l'ARecordIndex de l'enregistrement glissé
  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
// Autorisation de réception
    Accept := Source is TcxDragControlObject;
end;

procedure TForm1.cxGrid2DBTableView1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
// Affichage du texte de la ligne sur laquelle le Drop a été effectué
    self.Caption := TcxGridRecordCellHitTest(TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y)).GridRecord.Values[cxGrid1DBTableView1FULLNAME.Index];
end;

 Modification de la table à l’exécution

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

Suppression d’une colonne à l’exécution :

TableView.Columns[i].Destroy;

Ajout de bandes (bands) :

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

Définir une formule de calcul personnalisée pour FooterSummary

Il est possible de définir une formule de calcul personnalisée pour FooterSummary d’une cellule en utilisant d’autres FooterSummary dans l’événement DataControllerSummaryAfterSummary. Cela ne peut pas être fait dans l’événement OnGetText de la cellule elle-même, car au moment de l’appel, les Summary ne sont pas encore calculés.

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;

En ce qui concerne un tel calcul pour GroupSummary, il faudra parcourir récursivement tous les niveaux et calculer la formule nécessaire pour tous les GroupSummary à chaque niveau. Exemple d’implémentation.

Bugs

Oui, un composant aussi complexe ne peut pas en être exempt, et on peut parfois en rencontrer. Par exemple, dans certains cas, le Footer perdait les chiffres de poids fort lors de la sommation — au lieu de 14.8, par exemple, il affichait 4.8, indépendamment du format défini. Dans de tels cas, j’ai dû écrire ma propre routine de calcul de somme et l’appeler dans l’événement OnGetText sur 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

Afficher une infobulle dans le champ de saisie lui-même (.TextHint) — Cette propriété existe bien dans le composant, mais elle est masquée. Pour y accéder, il faut hériter la classe dans les déclarations :

type
  TcxLookupComboBoxAccess = class(TcxLookupComboBox); // pour accéder au champ masqué .TextHint

// Et par exemple dans OnCreate, effectuer l'assignation de l'infobulle :

TcxLookupComboBoxAccess(MyLookupCoboBox).TextHint := 'Texte de l''infobulle';

Ajouter un bouton supplémentaire personnalisé dans le champ de saisie, par exemple pour ouvrir un référentiel ou une fonction d’édition. Cela fonctionne également pour tous les contrôles ayant des Properties (Liste des contrôles ici)

with MycxLookupComboBox.Properties do begin
    Images := cxImageList;
    Result := Buttons.Add;
    Result.Default := True;
    Result.Kind := bkGlyph;
    Result.LeftAlignment := False;
    Result.Action := Action; // C'est l'action qui sera exécutée
end;

Laisser un commentaire

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