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;
