Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

La Grid Silverlight/WPF, cette inconnue si populaire...

[new:27/01/2011]Il étonnant de voir à quel point un composant de base tel que le contrôle “Grid” peut à la fois être aussi populaire (son utilisation est quasi obligatoire dans une application WPF ou Silverlight) et sembler nébuleux à une majorité de développeurs. Ce billet s’adresse plutôt aux débutants mais je suis certain que les plus confirmés en profiteront...

Demandez autour de vous, en commençant même par votre propre personne... Comment définir des lignes et des colonnes dans une Grid ? Que signifie le mode Canvas de la Grid sous Blend ? Quelle est la différence entre une largeur de colonne “Auto” et une autre en “*” (étoile) ?

Il n’y a pourtant pas milles choses à savoir sur la grille mais bizarrement son paramétrage parait opaque à beaucoup de gens ou, plus exactement, il est très rare de voir quelqu’un se servir intelligemment de toutes les possibilités de la Grid... Tout placer à l’œil ou utiliser les facilités d’en EDI comme Blend n’est pourtant pas la solution miracle à cette méconnaissance. Il arrive qu’on a besoin de mesure précise, de prévoir une mise en page bien particulière. Dès lors les rudiments de la Grid doivent être “instinctifs”, il ne devrait pas y avoir de question à se poser.

Quelque chose ne vas pas avec la grille j’en conviens. Si elle semble si opaque malgré sa grande simplicité c’est qu’il y a un mauvais choix quelque part...

Ce “mauvais choix” est de même nature que la syntaxe du Binding (cherchez ce terme dans le blog et vous découvrirez de nombreux articles traitant le sujet en profondeur). C’est à dire que la Grid ne se paramètre pas selon des propriétés claires et bien définies, mais par le biais d’une mini-syntaxe locale qui lui est propre. Tout comme le Binding (mais heureusement en moins compliqué que ce dernier !).

Si ce choix se comprend, XAML a ainsi introduit des langages dans son langage. Des choses mystérieuses qui s’écrivent comme des chaines de caractères, non contrôlées à la compilation et utilisant un logique et une syntaxe propre uniquement valable dans ces cas précis et qu’aucune aide de type IntelliSense ne vient vraiment seconder.

C’est pour moi une erreur, une perte de cohérence grave dans un environnement déjà complexe à maitriser. Il en résulte que le paramétrage de la Grid est presque aussi peu connu que les méandres du Bindind. Mais d'un autre côté Xaml est bien plus rigoureux et "prévisible" que HTML+CSS qui sont un enfer.

Comment vouloir créer des mises en pages sophistiquées sur un savoir dont les bases reposent sur des sables mouvants ? Comment apprendre à conduire une voiture alors qu’on hésite sur la façon de la démarrer ?

Alors reprenons les fondamentaux !

(NB: les exemples qui suivent sont très simples et sont tous réalisés en utilisant Kaxaml, un utilitaire gratuit bien pratique pour s’exercer, téléchargez-le pour reproduire les exemples et vous entrainer !)

La Grid

C’est avant tout  “the” conteneur XAML par excellence. Celui qui apparait par défaut dans toute nouvelle page comme élément racine.

C’est un conteneur rectangulaire. Jusque là rien ne le différencie d’un Canvas ou d’un StackPanel.

Mais la Grid est pourtant bien particulière : elle permet de définir des tableaux un peu dans le même esprit que les tables HTML.

Lignes et colonnes sont définies par des balises, comme pour une table HTML. Sans aucune définition précise une grille se compose en réalité d’une seule cellule de coordonnées (0,0).

Le placement des contrôles enfants dans les cellules s’effectue par le biais des commandes d’alignement de ces derniers (propriétés attachées par la grille à ses enfants) concernant les deux axes (vertical et horizontal). Une fois ces alignements déterminés, on peut jouer sur le positionnement d’un élément en ajustant ses marges (margin). A la différence du Canvas qui ne propose qu’un placement Top / Left, la grille propose plus de nuance dans les moyens de positionner les éléments dans les cellules. Bien entendu la grille offre aussi à ses enfants des propriétés permettant de décider dans quelle cellule ils doivent être placés (Grid.Row et Grid.Column). Il existe même des commandes RowSpan et ColumnSpan pour permettre à un contenu de s’étendre verticalement ou horizontalement sur plusieurs cellules.

La Grid joue aussi un rôle important sur le clipping et le redimensionnement des éléments présents dans les cellules.

Tout cela est déjà finalement assez complexe et non n’avons pas abordé la définition des colonnes et des lignes !

Placement simple

k0a45ecm

L’exemple ci-dessus définit une grille comprenant 4 colonnes et 3 lignes. Dans la cellule (0,0) nous avons placé un bouton et dans la cellule (1,2) un rectangle rouge.

La définition d’une telle grille se décompose en deux parties : d’une part la définition des lignes et colonnes, d’autre part les éléments enfants.

 

<Grid Width="300" Height="200" ShowGridLines="True" Background="Silver">
       <Grid.RowDefinitions>
           <RowDefinition Height="50"></RowDefinition>
           <RowDefinition Height="50"></RowDefinition>
           <RowDefinition Height="*"></RowDefinition>
       </Grid.RowDefinitions>
       <Grid.ColumnDefinitions>
           <ColumnDefinition Width="100"></ColumnDefinition>
           <ColumnDefinition Width="100"></ColumnDefinition>
           <ColumnDefinition Width="*"></ColumnDefinition>
           <ColumnDefinition Width="50"></ColumnDefinition>
       </Grid.ColumnDefinitions>
       <Button x:Name="btnTest" Width="50" Height="30" Content="Clic!" 
                                        Grid.Row="0" Grid.Column="0"></Button>
       <Rectangle x:Name="rectTest" Width="50" Height="30" Fill="Red" 
Grid.Row="2" Grid.Column="1"></Rectangle>
   </Grid>

La section <Grid.RowDefinitions> laisse comprendre qu’elle définit une collection de RowDefinition, c’est à dire de définitions de lignes. La section Grid.ColumnDefinitions fait de même pour la collection des définitions de colonnes.

Une grille se découpe ainsi comme une gros gâteau rectangulaire : par des coupes parallèles aux bords et donc orthogonales entre elles.

Mais l’un des autres problèmes que ce mode de définition pose est que le raisonnement semble être celui des “piquets et des intervalles”. On a l’impression de poser des piquets alors qu’en réalité on définit des intervalles... Si je plante trois piquets dans le sol, je ne définis uniquement que 2 intervalles. Or, ici dans la Grid, ce qui nous intéresse ce sont les intervalles (les cellules) et ce sont bien ces intervalles qui sont définis, non des piquets malgré leur représentation visuelle sous cette forme...

Ainsi, si on pourrait croire que si je définis 3 "piquets", au lieu de définir 2 intervalles, je devrais en définir 4 (car les bords comptent - entre 3+2 piquets il existe 4 intervalles). En fait, en définissant 4 ColumnDefinition je définis bien 4 intervalles, donc 4 cellules. De même pour les lignes, en définissant 3 RowDefinition, je définis trois lignes et non deux ou quatre.

Bien que ressemblant à une définition de “piquets”, la définition des lignes et des colonnes d’une Grid correspond à celle des intervalles donc au nombre exact de lignes et de colonnes désirées....

Regardez à nouveau la capture écran plus haut. J’ai activé l’affichage des “piquets” (ShowGrildLines=True). On voit bien 3 piquets verticaux. Comptez... oui il y a 4 cellules sur l’horizontale. Mais regardez le code XAML, il existe bien 4 lignes de ColumnDefinition. Des intervalles matérialisés comme des piquets, cela peut créer la confusion.

Donc on se rappelle : nombre de cellules sur un axe = nombre de définitions sur cette axe, tout simplement.

C’est à dire ici : nombre de cellules sur l’axe horizontal = 4 ColumnDefinition = 4 colonnes. De même nombre de cellules sur l’axe verticale = 3 RowDefinition = 3 lignes.

Ce qui est vraiment trompeur c'est qu'autant Visual Studio que Blend donnent l'impression de placer les "piquets" et de déplacer ces derniers... Mais en Xaml la notion de "piquet" n'existe tout simplement pas ! Si on est habitué à des logiciels comme Expression Design, PhotoShop, Illustrator, PaintShop..., tous ces logiciels de dessin permettent généralement de placer des lignes horizontales ou verticales qui peuvent être "magnétiques" ou non. Ici c'est bel et bien une logique de "piquets" et "brochettes". Ces lignes (repères) existent vraiment et possèdent des coordonnées précises. Quand on manipule une Grid Xaml, les EDI mettent à notre disposition un procédé visuel qui ressemble à celui des logiciels de dessin, mais ce n'est qu'une astuce visulle, les piquets et les brochettes n'existent pas, ils sont virtuels et découlent de la définition des intervalles !

Ainsi, lorsqu’on déplace un “piquet” dans l’EDI on ne fait que changer la valeur de l’intervalle (et de l’intervalle voisin!) et non la "position du piquet", position qui n'existe pas, pas plus que le piquet lui-même... Pour en supprimer un il suffit de cliquer sur son marqueur et de taper sur la touche de suppression. Supprimer un piquet fusionne deux lignes ou deux colonnes... parfois on met longtemps à le comprendre !

Bien entendu le raisonnement est rigoureusement le même sur les deux axes. Pour les horizontales on ne parlerait plus de piquets mais de “brochettes” par exemple. Les piquets qui sont verticaux matérialisent les colonnes qui s’étalent sur l’horizontale, les brochettes qui sont horizontales matérialises les lignes qui découpent la grille verticalement...

Définir les lignes et les colonnes

Dans le code publié à la section précédente on voit clairement les deux blocs définissant les lignes et les colonnes enchâssés dans leurs balises respectives RowDefinitions et ColumnDefinitions.

Prenons les lignes (Row). Chaque définition est constituée de la façon suivante :

<balise taille=”xx”/> 

“balise” vaut “RowDefinition” pour la définition d’une ligne et “ColumnDefinition” pour celle d’une colonne.

”taille” permet de définit la hauteur de la ligne (Height), elle est suivie du symbole égal puis, entre guillemets, d’une valeur. Nous verrons cette dernière plus loin.

Lorsqu’il s’agit d’une colonne, ce n’est plus “Height” qui est défini mais en toute logique “Width”. Le principe reste le même.

Comme tout langage de type XML, les balises XAML peuvent être fermées immédiatement (mon exemple ci-avant) ou bien être fermées par une balise fermante complète (exemple du code publié plus haut), cela ne change rien à la signification du code.

Les Valeurs des Dimensions

Là les choses peuvent varier et l’effet final sera très différent. C’est même ici que se joue la vraie subtilité de la Grid.

Si vous reprenez l’exemple publié plus haut, vous trouverez des définitions de lignes avec des valeurs entières (50 pour les 2 premiers intervalles et “étoile” pour le dernier). Les colonnes utilisent un mode équivalent : valeurs entières (100 et 50) et une valeur “étoile” mais pas en dernière position.

Les valeurs entières permettent de définir un marqueur de façon fixe, au pixel près.

Ainsi ‘Width=”100”’ définit une largeur de 100 pixels très exactement, tout comme ‘Height=”50”’ définit une hauteur de 50 pixels.

Cette façon de définir des colonnes et des lignes est particulièrement pratique lorsqu’on doit mettre en page des parties d’écran selon un positionnement rigoureux et immuable.

Ce n’est hélas que rarement le cas tant les résolutions et ratios d’écran sur lesquelles peuvent tourner les applications sont aujourd’hui d’une diversité hallucinante... Il est donc souvent préférable de concevoir des mises en pages souples dites “dynamiques”. Pour le développeur qui vient du monde du Desktop, il s’agit souvent d’un nouveau réflexe à prendre. Ceux qui viennent du développement Web sont rompus à cette problématique et à la gymnastique qu’elle impose.

L’étoile

Utilisée par deux fois dans l’exemple, l’étoile définit une colonne (ou une ligne) de largeur (ou hauteur) variable. Mais pas variable n'imporrte comment... L’étoile utilisée seule indique d’utiliser toute la place restante dans l’espace de la Grid.

Comme dans l’exemple nous avons définit une largeur fixe pour la grille (balise <Grid Width=”300”...) on peut calculer à coup sûr la taille de la colonne notée étoile : 300 – 100 – 100 – 50 = 50. Dans ce cas précis (une grille ayant une largeur fixe), l’intérêt de l’étoile est purement académique, nous aurions pu mettre 50 directement... Mais si la Grid est elle-même placée dans un autre conteneur et qu’elle n’a pas une taille fixe, la colonne définie avec une étoile aura une taille qu’il n’est pas possible, a priori, de connaitre.

En tout cas, ainsi définie, la grille possède une colonne et une ligne dont la taille variera automatiquement si les dimensions de la grille sont changées. Si on agrandit en hauteur la grille, c’est la dernière ligne (hauteur = étoile) qui s’agrandira, les autres conserveront leur taille (puisque fixée eu pixels). De même si on élargit la grille, c’est l’avant dernière colonne qui profitera du nouvel espace puisque c’est elle qui est marquée par l’étoile.

Ce mode de dimensionnement est particulièrement souple mais impose de bien “calculer son coup”, donc d’avoir un sketching assez précis de l’écran (ou du contrôle) final avant de se lancer à l’aveuglette !

Proportionnalité

Intervalle fixe, intervalle variable, il est aussi possible de définir des intervalles par pourcentage.

Dans ce cas la valeur est suivie d’une étoile. Cette dernière conserve donc le sens de “proportionnalité” qu’on lui a vu précédemment mais ici le développeur peut fixer la proportion.

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="2*"></ColumnDefinition>
    <ColumnDefinition Width="2*"></ColumnDefinition>
    <ColumnDefinition Width="5*"></ColumnDefinition>
     <ColumnDefinition Width="1*"></ColumnDefinition>
</Grid.ColumnDefinitions>

La somme des proportions doit être égale à 100% peut importe les nombres. Dans l’exemple ci-dessus la somme fait 10. Remplacez les valeurs par leur dixième (0,2; 0,2; 0;5 et 0,1) et vous obtiendrez exactement le même résultat !

Dans le mode proportionnel toutes les lignes (ou colonnes) ainsi définies se comportent comme la colonne “étoile” vu précédemment, sauf que les tailles relatives sont conservées (en fonction des valeurs de proportion saisies). Donc si la taille de la grille change, les cellules changeront de taille en conservant l’aspect général des intervalles. 

Taille automatique

A tous les modes déjà vus s’ajoute le mode automatique. Un intervalle défini en automatique se comportera de la façon suivante : toutes les cellules de ligne (ou de la colonne) prendront la taille du plus grand objet contenu dans cette ligne (ou cette colonne).

image

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="auto"></ColumnDefinition>
     <ColumnDefinition Width="auto"></ColumnDefinition>
     <ColumnDefinition Width="auto"></ColumnDefinition>
     <ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>

Comparez l’image ci-dessus avec l’image se trouvant plus haut. C’est un peu le jeu des 7 différences Smile

La première chose que l’on constate est que chaque colonne a une largeur identique à l’objet qu’elle contient (colonne 0 : le bouton, colonne 1 : le rectangle rouge).

Mais on peut aussi voir que derrière ces éléments, et bien que nous ayons défini 4 colonnes, il ne s’en trouve qu’une seule de visible qui prend “tout le reste” de l’espace. La colonne 2 (la troisième donc) existe bel et bien, mais s’adaptant à son contenu (qui est “rien”), elle prend une taille qui ne vaut.. rien, donc zéro. Il en va de même pour la dernière colonne. Mais comme la grille est un espace rectangulaire qui ne peut pas contenir de trou, l’espace semble être rempli (c’est la couleur grise choisie pour le background qui donne cet effet).

Egalité

Variable, automatique, proportionnel, un intervalle peut se définir de plusieurs façons. Mais il est aussi bien pratique parfois d’obtenir uniquement un quadrillage régulier.

Comme toujours en XAML il y a plus d’un chemin pour arriver au résultat. On pourrait définir toutes les colonnes (ou lignes) avec l’étoile :

<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>

On obtiendrait bien 4 colonnes de même dimension.

On pourrait aussi décomposer les proportions :

<ColumnDefinition Width="25*"></ColumnDefinition>
<ColumnDefinition Width="25*"></ColumnDefinition>
<ColumnDefinition Width="25*"></ColumnDefinition>
<ColumnDefinition Width="25*"></ColumnDefinition>

4 fois 25 fait 100, il y a bien 4 colonnes définies à 25%, donc 4 colonnes identiques.

Mais il y a plus simple :

<Grid.ColumnDefinitions>
   <ColumnDefinition />
   <ColumnDefinition />
   <ColumnDefinition />
   <ColumnDefinition />
</Grid.ColumnDefinitions>

 

Limiter les intervalles

Nous avons vu comment la Grid nous laisse jouer sur les tailles de ses cellules. La liberté est d’autant plus grande qu’il est possible de mixer les modes entre eux (avec une colonne fixe suivie d’une colonne étoile; avoir 3 colonnes proportionnelles et une 4ème automatique, etc...).

Mais cela ne s’arrête pas là. La Grid permet aussi de définir des tailles minimum et maximum pour chaque définition de ligne ou de colonne grâce aux propriétés MaxWidth et MawHeight.

image

 

<Grid Width="300" Height="200" ShowGridLines="True" Background="Silver">
   <Grid.RowDefinitions>
      <RowDefinition Height="50"/>
      <RowDefinition Height="50"/>
      <RowDefinition Height="*"/>
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition MinWidth="30" MaxWidth="150"/>
      <ColumnDefinition Width="Auto" MinWidth="30" MaxWidth="150"/>
      <ColumnDefinition Width="Auto" MinWidth="30" MaxWidth="150"/>
      <ColumnDefinition Width="Auto" MinWidth="30" MaxWidth="150"/>
    </Grid.ColumnDefinitions>
      
 <Button Width="200" Height="28" Content="Click me !" 
                                  Grid.Row="0" Grid.Column="0"/>
 <Rectangle Width="50" Height="30" Fill="Violet" 
                                  Grid.Row="2" Grid.Column="1"/>
</Grid>

Ici nous avons placé un bouton de 200 pixels de large dans une colonne dont la taille est limitée à l’étendue 30-150 pixels. De fait le bouton est coupé. La Grid sait donc effectuer un clipping automatique sur le contenu des cellules. Astuce à se rappeler lorsqu’on sait que le clipping n’est pas supporté de façon égale par tous les conteneurs (du moins automatiquement).

Dans le code exemple ci-dessus l’effet est rendu parce que tous les colonnes sont automatiques sauf la première. Toutes les indications de taille sont très sensibles à leur “environnement”. Par exemple si nous passons la première colonne en mode auto (en conservant toutes les autres définitions) nous obtiendrons cela :

image

C’est comme si l’indication de taille maxi de la colonne 0 n’était plus prise en compte...

De même, Si nous supprimons l’indication “auto” de toutes les colonnes, encore une fois sans rien changer d’autre, nous obtiendrons :

image

Toutes les colonnes ont la même taille...

Tout cela est assez délicat à bien maitriser (c’est à dire intégrer intelligemment l’ensemble de ces possibilités dans un design bien pensé). Pas si simplette que ça la Grid finalement...

Placement hors cellules

La Grid permet de définir des colonnes et des lignes format des cellules dans lesquelles on peut placer divers objets. Il n’est pas nécessaire d’insérer des grilles dans les cellules si on désire placer plusieurs objets dans celles-ci, mais qu’il s’agisse de Grid, de Canvas ou autre StackPanel cela rend le positionnement plus facile dans le cadre de mises en page dynamiques.

La Grid permet de définir assez librement le placement des objets dans les cellules avec de nombreuses options d’alignement (gauche, droit, haut, bas, stretch, center) et de marges. Elle autorise aussi l’étalement d’un objet sur plusieurs colonnes ou lignes :

image

<Button Width="200" Height="28" Content="Click me !" 
   Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"></Button>

En utilisant Grid.ColumSpan = “3” dans la définition du bouton j’ai indiqué que celui-ci devait s’étendre sur les 3 colonnes commençant à la colonne 0 (puisque le bouton est placé en Grid.Column=0).

FAQ

Peut-on avoir des lignes non pointillées ?

En réalité les lignes pointillées affichées par ShowGridLines n’ont pas vocation a être affichées dans une application terminée. Elles ne servent que d’aide au placement en mode conception. Pour ajouter une bordure à une cellule il faut y placer un rectangle ou un Border, de même si on souhaite dessiner les contours d’une Grid il faut la placer dans un Border par exemple. On peut aussi dessiner de simples lignes en utilisant un Path.

Qu’est ce que le mode layout ou Canvas de la Grid ?

Il s’agit d’une astuce de Expression Blend. Quand la grille est en mode Layout, le déplacement d’un “piquet”, en forçant le changement de taille des cellules qu’il définit, force à un repositionnement des éléments éventuellement contenus dans les cellules concernées. En mode Layout les objets vont changer de taille car c’est leur Margin qui est conservée.

En mode Canvas, Expression Blend ne cherche plus à préserver les marges mais au contraire le positionnement et la taille des objets. Tout naturellement ce sont alors les marges des objets qui sont modifiées...

Peut-on placer un objet à cheval sur deux cellules ?

Nous avons vu que le RowSpan ou le ColumnSpan permettaient de placer un objet en le faisant s’étaler sur plusieurs lignes ou colonnes, c’est une approche. Quand celle-ci ne convient pas et qu’on veut pouvoir placer un objet n’importe où dans la grille sans se soucier des cellules une des solutions est tout bêtement de poser l’objet sur le conteneur parent de la Grid et de le positionner exactement où on le souhaite (avec un z-order supérieur à celui de la grille bien entendu).

Grid ou Canvas ?

Les mises en page se font avec des grilles et non des Canvas car seule la grille gère les proportions, le clipping des cellules, etc, c’est à dire de nombreuses options très utiles lorsqu’il s’agit de faire de la mise en page dynamique (ou non d’ailleurs). Le Canvas est un conteneur “bête” n’acceptant qu’un  positionnement Top/Left, sans clipping. Bête dans le sens de simple, mais très utile lorsqu’il s’agit de positionner des dessins au pixel près en étant sûr que rien ne viendra modifier ce placement. Toutefois une grille avec une seule cellule peut être aussi utilisée (en bénéficiant supplémentaires).

Conclusion

Il y aurait encore beaucoup de choses à dire sur le contrôle Grid. Versatile, parfois déroutant, il reste le contrôle de base utilisé de façon privilégiée pour toute mise en page.

Ce billet s’adressant avant tout aux débutants, j’espère que les lecteurs confirmés y auront malgré tout trouvé matière à réflexion...

Stay Tuned !

blog comments powered by Disqus