La
libraire Qt - 4
Les layouts
Le mois dernier nous avons appris à créer nos propres widgets, et au passage une méthode pour organiser automatiquement les widgets dans une fenêtre. Je vous propose de découvrir aujourd’hui une autre méthode, un peu plus délicate, mais plus souple et plus puissante.
L’inconvénient
de QGrid
, QHBox
ou QVBox
est que si ces classes
permettent de disposer les widgets, elles ne s’occupent
guère de l’espace qu’ils occupent - notamment, en
cas de redimensionnement. Essayez d’agrandir ou réduir
la fenêtre du convertisseur euro du mois dernier : ce n’est
pas très esthétique.
Je vous propose un exemple coloré, pour bien voir ce qui se passe. Nous allons afficher une série de boutons, chacun d’une certaine couleur, et disposés « harmonieusement ». Les couleurs dans Qt sont décrites par la classe QColor, qui propose quelques outils permettant de les manipuler (éclaircir, assombrir, composantes RGB ou HSV, etc.). Voici un premier programme :
01: #include <qlayout.h> 02: #include <qwidget.h> 03: #include <qpushbutton.h> 04: #include <qcolor.h> 05: #include <qapplication.h> 06: #include <qsizepolicy.h> 07: typedef struct 08: { char* Nom ; 09: QColor Couleur ; 10: } NomCouleur ; 11: int main (int argc, char* argv[]) 12: { QApplication app(argc, argv) ; 13: QWidget w ; 14: NomCouleur tab[] = 15: {{"rouge", Qt::red}, {"vert", Qt::green}, {"bleu", Qt::blue}, 16: {"jaune", Qt::yellow}, {"cyan", Qt::cyan}} ; 17: QHBoxLayout * hbox_1 = new QHBoxLayout(&w) ; 18: QPushButton* bouton = 0 ; 19: int i ; 20: for (i = 0 ; i < 3 ; i++) 21: { bouton = new QPushButton(tab[i].Nom, &w) ; 22: bouton->setPaletteBackgroundColor(tab[i].Couleur) ; 23: hbox_1->addWidget(bouton) ; 24: } 25: app.setMainWidget(&w) ; 26: w.show() ; 27: return app.exec() ; 28: }
lignes 1 à 6 : les
fichiers d’en-têtes dont nous avons besoin. Remarquez en
particulier <qlayout.h>
et
<qcolor.h>
, qui sont nouveaux ce
mois-ci.
lignes 7 à 10 : juste une petite structure pour avoir un code à peu près lisible : on associe une chaîne de caractères à une couleur. Un tableau de couleurs est déclaré et rempli lignes 13 à 15. Nous utilisons ici des constantes symboliques définie dans la classe Qt. Les couleurs directement disponibles ainsi sont black (noir), white (blanc), darkGray (gris sombre), gray (gris), lightGray (gris clair), red (rouge), green (vert), blue (blue), cyan, magenta, yellow (jaune), darkRed, darkGreen, darkBlue, darkCyan, darkMagenta et darkYellow (versions assombries). Vous pouvez obtenir n’importe quelle couleur en donnant les composantes RGB au constructeur de QColor.
ligne 17 : voici la classe qui nous intéresse : QHBoxLayout. Cette classe est utilisée pour aligner des « choses » horizontalement. À la différence de QHBox évoquée le mois dernier, cette classe ne dérive pas de QWidget : ce n’est donc pas un widget. Elle fait partie de la famille des layouts, qui sont des outils de positionnement de widgets ou d’autres layouts. Nous verrons cela plus loin. Remarquez que l’instance hbox_1 est tout de même attachée à un widget, donné dans le constructeur.
lignes 20 à 24 : création d’une première série de boutons. Chaque bouton est attaché au widget parent w (donné dans le constructeur). La méthode setPaletteBackgroundColor (héritée de QWidget, donc disponible dans tous les widgets de Qt) permet de définir la couleur de fond. Enfin, chaque bouton est « ajouté » au layout déclaré précédemment : on indique de cette manière à hbox_1 ce qu’il doit disposer.
Le reste du code devrait vous être maintenant familier. Remarquez l’absence d’instruction delete : à la destruction automatique du widget principal w, celui-ci se chargera de détruire tout ce qui lui est attaché, les layouts comme les widgets enfants.
Compilez, exécutez, voyez :
Si vous étendez la fenêtre horizontalement, les boutons changent de taille :
Par contre, si vous l’étendez verticalement, ils ne suivent pas le mouvement :
Remarquez que vous ne pouvez plus réduire la fenêtre de manière à masquer une partie de son contenu. Personnalisons un peu tout cela.
Si les boutons ne s’étendent pas verticalement, c’est que la classe QPushButton définie une politique de redimensionnement (size policy) précisant cela : extension autorisée en largeur mais pas en hauteur, et réduction interdite si elle risque de masquer le texte. Les zones de textes QLabel autorisent par contre l’extension verticale.
Cette politique de redimensionnement est stockée
dans la classe QSizePolicy,
dont tout widget possède une instance. Les classes de
la famille layouts utilisent beaucoup ces informations. Cette
classe est déclarée dans <qsizepolicy.h>
.
Insérez ces trois lignes juste après la boucle for :
1: QSizePolicy size_pol = bouton->sizePolicy() ; 2: size_pol.setVerData(QSizePolicy::MinimumExpanding) ; 3: bouton->setSizePolicy(size_pol) ;
Nous modifions la politique de redimensionnement verticale du dernier bouton créé dans la boucle (le bleu en l’occurrence), pour qu’il utilise tout l’espace qu’on lui donne. Compilez et exécutez :
Cette fois la fenêtre a une hauteur par défaut, et le bouton occupe cet espace. Si vous redimensionner la fenêtre, le bouton bleu suit les changements. Consultez la documentation de QSizePolicy pour voir quelles sont les possibilités. La politique horizontale d’un widget peut être obtenue par QWidget::horData(), et définie par QWidget::setHorData().
Peut-être trouvez-vous que les boutons sont trop collés l’un à l’autre, et qu’il faudrait les séparer des bords de la fenêtre (qui sont aussi les bords du widget principal). Changez la ligne 17, pour donner un nouvel argument au constructeur de QHBoxLayout :
QHBoxLayout * hbox_1 = new QHBoxLayout(&w, 8) ;
Maintenant les boutons sont décollés de la fenêtre et espacés ! Les espaces sont, vous l’aurez deviné, de 8 pixels exactement. En fait ce paramètre défini la marge extérieure placée autour du layout, et s’il n’est suivi d’aucune autre valeure, il défini aussi l’espacement intérieur. Essayez ceci :
QHBoxLayout * hbox_1 = new QHBoxLayout(&w, 8, 30) ;
Les boutons sont beaucoup plus espacés ! Et si vous redimensionnez la fenêtre, les boutons s’agrandissent également, marges et espacements demeurants fixes.
Les valeurs données au constructeur du layout sont globales. Vous pouvez insérer un espacement quelconque entre deux éléments à l’aide de la méthode insertSpace :
QHBoxLayout * hbox_1 = new QHBoxLayout(&w) ; QPushButton* bouton = 0 ; hbox_1->addSpacing(10) ; bouton = new QPushButton(?rouge?, &w) ; bouton->setPaletteBackgroundColor(Qt::red) ; hbox_1->addWidget(bouton) ; hbox_1->addSpacing(20) ; bouton = new QPushButton(?vert?, &w) ; bouton->setPaletteBackgroundColor(Qt::green) ; hbox_1->addWidget(bouton) ; hbox_1->addSpacing(30) ;
Ce qui donne ce résultat :
Plus de marges en haut et en bas, mais des marges à gauche et à droite. Si vous étendez la fenêtre horizontalement, remarquez que les espaces restent fixes, tandis que les boutons s’agrandissent.
Ce comportement n’est pas toujours souhaitable, parfois on préfère que ce soient les espacements qui varient avec la taille de la fenêtre. Qt permet cela, en définissant des élastiques entre widgets :
01: QHBoxLayout * hbox_1 = new QHBoxLayout(&w) ; 02: QPushButton* bouton = 0 ; 03: hbox_1->addStretch(1) ; 04: bouton = new QPushButton(?rouge?, &w) ; 05: bouton->setPaletteBackgroundColor(Qt::red) ; 06: hbox_1->addWidget(bouton) ; 07: hbox_1->addStrech(2) ; 08: bouton = new QPushButton(?vert?, &w) ; 09: bouton->setPaletteBackgroundColor(Qt::green) ; 10: hbox_1->addWidget(bouton) ; 11: hbox_1->addStretch(1) ;
Maintenant les boutons ne changent pas de taille, c’est l’espacement qui varie :
Remarquez que l’espacement entre les deux boutons est toujours double de l’espacement contre les bords : c’est du au fait que nous avons donné un facteur d’élasticité double à cette espace, ligne 7. Si nous avions indiqué 3, l’espacement serait triple. Si nous donnons 2 en lignes 3 et 11, et 3 en ligne 7, alors l’espacement intérieur sera égale à 3/2 de l’espacement extérieur... Le facteur d’élasticité est donc relatif.
Naturellement, rien n’empêche de combiner espacement fixe et espacement élastique. C’est même la seule façon de garantir qu’un certain espace ne sera pas réduit à 0. Par exemple, pour avoir un espacement de 10 à gauche, de 20 entre les boutons, et l’espace à droite extensible mais avec un minimum de 10 pixels, il suffit de changer la ligne 3 en :
hbox_1->addSpacing(10) ;
...de changer la ligne 7 en :
hbox_1->addSpacing(20) ;
...et de changer la ligne 11 en ces deux lignes :
hbox_1->addSpacing(10) ; hbox_1->addStretch(1) ;
Ainsi les boutons restent espacés, et collés sur la gauche.
Si vous voulez néammoins que les widgets demeurent extensibles, il suffit de donner un paramètre supplémentaire à la méthode addWidget(), précisément un facteur d’élasticité (toujours relatif donc). Par exemple, si je veux que mon bouton vert s’étende du double de l’espacement à droite, il suffit de changer la ligne 10 ainsi :
hbox_1->addWidget(bouton, 2) ;
À vous de trouver la bonne combinaison pour avoir une présentation harmonieuse !
Nous n’avons jusqu’ici utilisé qu’un layout horizontal, pour n’organiser que des widgets. Mais voyez ceci :
01: 1.QVBoxLayout * vbox = new QVBoxLayout(&w) ; 02: 2.QPushButton* bouton = 0 ; 03: 3.int i ; 04: 4.QHBoxLayout * hbox_1 = new QHBoxLayout(vbox) ; 05: 5.for (i = 0 ; i < 3 ; i++) 06: 6.{ bouton = new QPushButton(tab[i].Nom, &w) ; 07: 7. bouton->setPaletteBackgroundColor(tab[i].Couleur) ; 08: 8. hbox_1->addWidget(bouton) ; 09: 9.} 10: 10.QHBoxLayout * hbox_2 = new QHBoxLayout(vbox) ; 11: 11.for (i = 3 ; i < 5 ; i++) 12: 12.{ bouton = new QPushButton(tab[i].Nom, &w) ; 13: 13. bouton->setPaletteBackgroundColor(tab[i].Couleur) ; 14: 14. hbox_2->addWidget(bouton) ; 15: 15.}
Dans le détail :
ligne 1 : nous créons cette fois un layout vertical avec QVBoxLayout, déclarée aussi dans qlayout.h ; son utilisation est très similaire à celle de QHBoxLayout ;
lignes 4 et 10 : nous retrouvons les layouts horizontaux. Remarquez que nous donnons cette fois le layout vertical comme parent, et non le widget qui contient le tout.
Le reste du code est du déjà vu.
Ce code donne le résultat suivant, où j’ai un peu étendu la fenêtre dans les deux directions :
Dans chaque layout horizontal, les widgets se
comportent comme précédemment : ils s’étendent
dans une direction (parce que QPushButton
est contrainte dans l’autre). Les layouts horizontaux
sont eux-mêmes organisés par le layout vertical,
comme s’il s’agissait de widgets « normaux
». Vous pouvez librement utiliser les méthodes
addSpacing() et
addStretch() pour
organiser tout cela comme bon vous semble.
Dernier mot concernant ces layouts linéraires : vous pouvez insérer un widget entre deux autres à l’aide de la méthode insertWidget(), qui prend en paramètre la position d’insertion et le widget (et un facteur d’élasticité optionnel). Par exemple, pour insérer un bouton entre les deux widgets du bas :
bouton = new QPushButton("nouveau", "w) ; hbox_2->insertWidget(1, bouton) ;
La position 0 correspondant à « avant le premier widget ».
Pour être complet, notons que QHBoxLayout et QVBoxLayout dérivent de QBoxLayout, dont le constructeur prend en deuxième paramètre la direction d’organisation : de gauche à droite, de haut en bas, de bas en haut... La déclaration suivante est parfaitement équivalente à un QHBoxLayout tel que nous l’avons utilisé ici :
QBoxLayout box(&w, QBoxLayout::LeftToRight) ;
Les autres paramètres (optionnels) sont la marge et l’espacement.
Les autres valeurs possibles pour ce deuxième paramètres sont RightToLeft (droite à gauche), TopToBottom (haut en bas, celui de QVBoxLayout), BottomToTop (bas en haut), Down = TopToBottom et Up = BottomToTop.
Il existe dans les layouts l’équivalent du QGrid que nous avons vu le mois dernier : QGridLayout. Voici un exemple :
01: QGridLayout * grid = new QGridLayout(&w) ; 02: QPushButton* bouton = 0 ; 03: bouton = new QPushButton("rouge", &w) ; 04: bouton->setPaletteBackgroundColor(Qt::red) ; 05: grid->addWidget(bouton, 0, 0) ; 06: bouton = new QPushButton("vert", &w) ; 07: bouton->setPaletteBackgroundColor(Qt::green) ; 08: grid->addWidget(bouton, 1, 0) ; 09: bouton = new QPushButton("bleu", &w) ; 10: bouton->setPaletteBackgroundColor(Qt::blue) ; 11: grid->addWidget(bouton, 2, 0) ; 12: bouton = new QPushButton("jaune", &w) ; 13: bouton->setPaletteBackgroundColor(Qt::yellow) ; 14: grid->addWidget(bouton, 2, 1) ; 15: bouton = new QPushButton("cyan", &w) ; 16: bouton->setPaletteBackgroundColor(Qt::cyan) ; 17: grid->addWidget(bouton, 2, 2) ; 18: bouton = new QPushButton("vert", &w) ; 19: bouton->setPaletteBackgroundColor(Qt::green) ; 20: grid->addWidget(bouton, 1, 0) ; 21: QLabel * label = new QLabel("grand blanc", &w) ; 22: label->setAlignment(Qt::AlignCenter) ; 23: label->setPaletteBackgroundColor(Qt::white) ; 24: grid->addMultiCellWidget(label, 0, 1, 1, 2) ;
Ce qui produit :
ligne 1 : nous créons une instance de QGridLayout. Le parent peut être un widget comme ici, ou un layout de type quelconque. Normalement, il faut ensuite donner le nombre de lignes et de colonnes, mais la grille s’étend à la demande. On peut également indiquer après la marge extérieur et l’espacement intérieur.
lignes 5, 8, 11, 14, 17 et 20 : les widgets sont insérés dans les cellules de la grille avec addWidget(), en donnant la ligne et la colonne (dans cet ordre !).
ligne 24 : on peut insérer un widget de manière à ce qu’il couvre plusieurs cellules avec addMultiCellWidget(), en donnant (dans cette ordre) la ligne de départ, la ligne d’arrivée, la colonne de départ puis la colonne d’arrivée. Pour l’exemple, j’ai utilisé un QLabel, qui s’étend dans les deux directions librement.
Vous pouvez insérer des espacements entre lignes avec addRowSpacing() et entre colonnes avec addColSpacing(), en donnant l’indice et la valeur de l’espacement. Il s’agit dans ce cas d’un espacement minimum, qui peut changer si le widget qui contient la grille change de taille.
Vous voilà équipé pour construire des interfaces complexes, qui s’organisent (presque) toutes seules et réagissent correctement aux changements de taille. Le mois prochain, nous aborderons des widgets plus sophistiqués, comme les zones de défilement.
Yves Bailly
Article publié dans LinuxMagazine 40 de juin 2002