Étiquette : checkbox

  • Ajouter du texte dans une DataGridViewCheckBoxCell

    Tout d’abord expliquons pour j’ai ce besoin. Pour un projet (pro) une colonne de mon DataGrid affiche en information textuelle, une valeur numérique modifiable. Oui mais voilà sous certaines conditions, certaines lignes de cette colonne ne peuvent avoir que 0 ou 1 comme valeur numérique. En m’abonnant à l’évènement DataBindingComplete, j’ai donc modifié à la volée ces cellules en cellule avec une case à cocher (DataGridViewCheckBoxCell). Bien entendu, je trouvais cela un peu moins parlant. Une ligne avec une valeur numérique pouvait être suivie d’une case à cocher dans la même colonne. A la base, l’idée de la case à cocher, c’était surtout pour aider au changement de la valeur. Pourquoi donc éditer ce texte si on ne peut que mettre un 1 ou 0 ? Pour rendre plus claire cette colonne, j’ai donc eu l’idée de faire apparaître ce 1 ou ce 0 à côté de la case à cocher.

    Bien entendu cet objet n’existe pas de base dans le framework. Comme je sais qu’il ne faut pas réinventer la roue, je suis parti sur le net à la conquête de la chose. Et j’ai trouvé quelque chose, mais qui ne correspondait pas exactement à mes besoins… forcément ! Mais cette source d’inspiration m’a été forte utile.

    L’inspiration

    Extending The DataGridViewCheckBoxCell To Include Text

    Dans cet article l’auteur veut faire la même chose, mais pas pour le même besoin. Au final, comme le montre sa capture finale,  il semble vouloir avoir une liste de choix dans une ligne. Pour faire simple, le texte mis à coté de la case à cocher est un simple texte statique. Sa solution semble donc réellement utile que dans le cas d’une liste de choix.

    Je vais tout de même me baser sur sa solution du moins en point de départ, car au final, j’ai pas mal modifié (perfectionné ?) certains points.
    Dans la suite, mes commentaires seront donc beaucoup basé sur son exemple.

    Ma solution

    Tout comme Murray, je vais donc avoir besoin de définir 2 classes : DataGridViewCheckBoxTextColumn et DataGridViewCheckBoxTextCell. En effet, pour avoir une solution générique, la définition de la colonne correspondante est à faire. Ces 2 classes étendront donc respectivement les classes DataGridViewCheckBoxColumn et DataGridViewCheckBoxCell.

    DataGridViewCheckBoxTextCell

    Tout comme Muray, on peut commencer par définir cette classe. C’est très simple, on modifie juste le template de la cellule par défaut et le tour est joué.

    using System;
    using System.Windows.Forms;
    
    namespace Kerlink.Windows.Forms
    {
        public class DataGridViewCheckBoxTextColumn : DataGridViewCheckBoxColumn
        {
            public DataGridViewCheckBoxTextColumn()
            {
                this.CellTemplate = new DataGridViewCheckBoxTextCell();
            }
        }
    }

    DataGridViewCheckTextCell

    Voilà le gros du travail. Contrairement à Murray, je ne vais pas commencer par ajouter des propriétés à cette classe. Tout comme lui, j’ai envie de faire quelque chose de générique, j »ajouterais donc des propriétés au fur et à mesure que si cela est réellement nécessaire. Je vais tenter d’utiliser au maximum ce qui est déjà définie dans mon objet de base, la classe DataGridViewCheckBoxCell. Bref, ici je n’ajoute donc pas les propriétés Enabled, Text, Color et Font définie par Murray. Notre classe est donc déjà bien différente dans sa définition de base :

    public class DataGridViewCheckAndTextCell : DataGridViewCheckBoxCell
    {
        public DataGridViewCheckAndTextCell()
        {}
    }

    Passons donc directement à la deuxième action proposée par Murray : surcharger la méthode DataGridViewCheckBoxCell.Paint(). Nous allons donc en premier lieu appelé la méthode mère (qui fait ce qu’elle aura à faire, cela ne nous regarde pas) puis ajouter notre mixture locale. Voilà la bête, qui est bien différent de celle de Murray, plus courte et plus basée sur les propriétés existantes de la classe. De plus le placement du texte sera un peu plus propre (je trouve), c’est à dire correctement aligner (verticalement) avec les autres cellules de texte, contrairement au résultat de la solution de Murray. Sans plus tarder, le code :

    protected override void Paint(System.Drawing.Graphics graphics, System.Drawing.Rectangle clipBounds, System.Drawing.Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
    {
        // Call the base method (which must draw the checkbox)
        base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
    
        // Get the actual content Area (area where the checkbox must be)
        System.Drawing.Rectangle contentBounds = this.GetContentBounds(rowIndex);
    
        // Get Area Where String Must be Added (at 2px on the right of the checkbox)
        System.Drawing.Rectangle stringArea = new System.Drawing.Rectangle();
        stringArea.X = cellBounds.X + contentBounds.Right + 2;
        stringArea.Y = cellBounds.Y + contentBounds.Top;
        stringArea.Height = contentBounds.Height;
        stringArea.Width = cellBounds.Width - contentBounds.Right - 2 - 2;
    
        // Convert DataGridViewContentAlignment to StringFormat
        System.Drawing.StringFormat format = cellStyle.Alignment.ToStringFormat();
    
        // Draw the cell value inside the computed area
        graphics.DrawString(this.Value.ToString(), cellStyle.Font, new System.Drawing.SolidBrush(cellStyle.ForeColor), stringArea, format);
    }

    Ce qu’on fait est donc très simple. Contrairement à Murray, pas besoin de redessiner la checkbox. On a pas de notion de checkbox Enabled: ici, ce n’est pas une liste de choix. On va donc directement dessiner notre texte. On va donc passer tout sa partie de vérification des propriétés et d’initialisation de variable. On commence directement à définir notre espace où le texte doit être dessiner, et non pas un point de départ comme Muray. La définition d’une zone nous aidera pour l’alignement du texte dans cette dernière.
    On récupère donc la zone occupée par le contenu actuel : contentBounds via l’appel à GetContentBounds. On peut alors définir notre rectangle qui se situera à 2 pixel à droite du contenu actuel, de même hauteur que le contenu actuel, et de longueur maximum, c’est à dire ce qui reste de disponible dans la cellule en gardant une marge de 2 pixels sur sa droite.

    A ce stade, afin de dessiner le texte, il nous manque certains paramètre par rapport à Murray : le texte, la police et sa couleur. Comme précisé plus haut, je vais utiliser les paramètres déjà définies dans la classe ! Il suffit de les trouver… Ainsi, pour le texte, on utilisera donc la valeur actuelle associé à notre DataGridViewCheckBox. En effet dans mon cas, cette valeur est soit le chiffre 1 ou 0. Pour la police, rien ne sert d’ajouter une propriété, le paramètre cellSyle (de type DataGridViewCellStyle) de la méthode Paint contient déjà cette information. En effet, toute DataGridViewCell a une telle propriété. De même pour la couleur.

    Pour finir, par rapport à Murray, on dessine notre texte dans une surface. Cela nous permet donc d’aligner horizontalement et verticalement ce texte dans cette surface. La méthode DrawString utilise le type StringFormat pour se faire. Une propriété d’alignement existe déjà dans notre style de cellule, mais elle est de type DataGridViewContentAlignement. Une petite méthode d’extension (ou une méthode statique si vous êtes en .net2) et le tour est joué.

    On est donc en mesure de dessiner notre texte via l’appel à DrawString de l’objet graphics.

    Pour information, ci joint le code du convertisseur de type :

    public static class DataGridViewContentAlignmentHelper
    {
        public static System.Drawing.StringFormat ToStringFormat(this DataGridViewContentAlignment alignement)
        {
            System.Drawing.StringFormat format = new System.Drawing.StringFormat();
    
            // Set the Vertical Alignement (could transform enum in string and test value ?)
            switch (alignement)
            {
                case DataGridViewContentAlignment.BottomCenter:
                case DataGridViewContentAlignment.BottomLeft:
                case DataGridViewContentAlignment.BottomRight:
                    format.LineAlignment = System.Drawing.StringAlignment.Far;
                    break;
    
                case DataGridViewContentAlignment.MiddleCenter:
                case DataGridViewContentAlignment.MiddleLeft:
                case DataGridViewContentAlignment.MiddleRight:
                case DataGridViewContentAlignment.NotSet:
                    format.LineAlignment = System.Drawing.StringAlignment.Center;
                    break;
    
                case DataGridViewContentAlignment.TopCenter:
                case DataGridViewContentAlignment.TopLeft:
                case DataGridViewContentAlignment.TopRight:
                    format.LineAlignment = System.Drawing.StringAlignment.Near;
                    break;
            }
    
            // Set the Horizontal Alignement (could transform enum in string and test value ?)
            switch (alignement)
            {
                case DataGridViewContentAlignment.BottomCenter:
                case DataGridViewContentAlignment.MiddleCenter:
                case DataGridViewContentAlignment.TopCenter:
                    format.Alignment = System.Drawing.StringAlignment.Center;
                    break;
    
                case DataGridViewContentAlignment.BottomLeft:
                case DataGridViewContentAlignment.MiddleLeft:
                case DataGridViewContentAlignment.TopLeft:
                    format.Alignment = System.Drawing.StringAlignment.Near;
                    break;
    
                case DataGridViewContentAlignment.BottomRight:
                case DataGridViewContentAlignment.MiddleRight:
                case DataGridViewContentAlignment.NotSet:
                case DataGridViewContentAlignment.TopRight:
                    format.Alignment = System.Drawing.StringAlignment.Far;
                    break;
            }
    
            return format;
        }
    }

    1er essai

    Contrairement à Murray on a pas fini. Certes la colonne est disponible depuis le designer. Certes on voit la magie apparaitre. Mais comme on pouvait s’y attendre, il manque tout le côté dynamique. On a beau cocher ou décocher, pas de changement du texte associé.

    La première question à se poser, c’est comment fait donc une DataGridViewCheckBoxCell pour changer sa valeur quand celle-ci n’est pas un simple boolean ?

    On cherche, et la réponse est à porté de main ou plutôt à portée de 2 propriétés :

    • TrueValue : Obtient ou définit la valeur sous-jacente correspondant à une valeur de cellule true.
    • FalseValue :  Obtient ou définit la valeur sous-jacente correspondant à une valeur de cellule false.

    Du même acabits, on a aussi la IndeterminateValue.

    Une fois la databinding en place, on peut donc aussi préciser comment notre checkbox met à jour sa donnée. Ce code est donc du côté du client de notre classe. Dans mon cas, je devrais préciser une TrueValue à 1 et une FalseValue à 0. A noter que ce client peut le définir via les même propriétés disponible au niveau de la DataGridViewCheckBoxColumn.

    2ème essai

    La mise à jour a lieu certes. Mais celle ci n’opère que quand on sort du mode édition de la cellule (en cliquant sur une autre). On abouti là sur une grosse limite des CheckBox cell qui semble connue. En effet le chemin habituel google -> stackoverflow m’a fait aboutir sur cette solution.

    Contexte Dynamique

    Il faut donc s’abonner à l’événement CurrentCellDirtyStateChanged du DataGridView pour forcer la validation du changement en cours.

    Oui, mais voilà la propriété interne DataGridView  n’est pas disponible lors de la construction de notre classe. Pour récupérer son instance, on va donc surcharger la méthode protégée OnDataGridViewChanged.

    Passons donc au code :

    private DataGridView _previousDataGridView = null;
    // get datagridview instance to can subribe to CurrentCellDirtyStateChanged event.
    protected override void OnDataGridViewChanged()
    {
        base.OnDataGridViewChanged();
    
        if (this.DataGridView != _previousDataGridView)
        {
            if (_previousDataGridView != null)
            {
                // unsubscribe from previous DataGridView
                _previousDataGridView.CurrentCellDirtyStateChanged -= DataGridView_CurrentCellDirtyStateChanged;
            }
    
            if (this.DataGridView != null)
            {
                   // subscribe to new related DataGridView
                   this.DataGridView.CurrentCellDirtyStateChanged += new EventHandler(DataGridView_CurrentCellDirtyStateChanged);
            }
        }
    }
    
    // Force the databound property update (for this checkbox) as soon as the check box is checked/unchecked (default is after the CellLeave event)
    void DataGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e)
    {
        // we can't use this.DataGridView : it can be null in this context !
        DataGridView dgv = sender as DataGridView;
    
        // Always directly commit the uncommited changes of the current cell.
        if ((dgv.CurrentCell == this) && (dgv.IsCurrentCellDirty))
        {
            dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
        }
    }

    Voilà pas grand chose à ajouter pour expliquer. Au niveau de OnDataGridViewChanged, j’ai fait ça propre : je me désinscris de l’ancien pour m’inscrire sur le nouveau. Je sais pas si un changement est possible, mais en tout cas, cette méthode peut être appelé alors que this.DataGridView est nul en début d’exécution. Bref, j’ai préférais assurer le coup.

    Enfin dans le traitement de l’évènement CurrentCellDirtyStateChanged, on applique la solution trouvée. Seul remarque, on ne peut pas utiliser this.DataGridView ici. Bref, soit on récupére la DataGridView via le sender (ce que j’ai fait), ou on pourrait aussi utiliser notre variable locale _previousDataGridView.

    3ème essai

    C’est parfait. On clique, la valeur change directement. Good job.

    Au final

    Ces petites recherches ont été intéressantes. Comme d’habitude on prend des bouts de solution à droite à gauche pour faire ce qu’on veut. On pourrait imaginer une classe un peu plus générique s’adaptant aussi bien au besoin de Murray ou au miens (changement de comportement via une Propriété ?). Enfin, en réfléchissant à ça, je me demande aussi si cela reste toujours une bonne idée de partir de DataGridViewCheckBoxCell. En héritant de DataGridViewCell, on aura pas mal de chose à refaire (tout ce qui définie la checkbox) mais au moins on serait pas obligé d’utiliser des astuces pour réussir à se rendre compte du changement d’état de la CheckBox. En la gérant totallement on pourrait dire en revoir à l’évènement CurrentCellDirtyChanged. Un évènement lié à notre CheckBox serait directement utilisé… Voilà une autre solution envisageable.