Licence CC BY-NC-SA

Le positionnement grâce aux layouts

Dernière mise à jour :
Auteur :
Catégorie :

Il est temps de complexifier un peu nos fenêtres. Car un widget c’est bien mais… Ça pourrait être mieux ! Mais pour en ajouter, il va nous falloir étudier les layouts.

Le positionnement, c’est toute une science. On peut facilement se perdre dans la multitude des options proposées. C’est pour ça que l’on va utiliser un constructeur d’interface, Glade, dans quelques chapitres pour nous simplifier la vie. Mais pour le moment, nous allons tout faire à la main (et je sais que vous aimez ça ! :D ).

Introduction

La plupart des bibliothèques GUI requièrent de vous de placer précisément vos widgets dans une fenêtre, en utilisant le positionnement absolu ou relatif, GTK+ vous propose une approche du problème différente. Plutôt que de préciser la taille et la position d’un widget, vous allez les organiser en ligne ou en colonne par rapport aux autres. Vous pourrez évidemment forcer un widget de prendre la taille que vous souhaitez si vous en avez réellement le besoin, mais GTK+ n’a pas été pensé pour ça. Pour ce positionnement, nous allons utiliser des conteneurs.

De ce placement intelligent s’en résulte quelques propriétés :

  • La taille de votre widget sera déterminée par la taille de son contenu. Une taille minimale pourra être spécifiée.
  • Un widget occupera le maximum de place qu’il pourra. Il remplira l’espace qui lui est fournit.
  • La largeur et la hauteur d’une ligne ou d’une colonne d’un conteneur sera définie à partir du plus large/haut widget qu’il contient.
  • La taille de votre fenêtre sera automatiquement calculée en fonction de la taille des widgets qu’elle contient.

Ça peut paraître restrictif et compliqué comme ça, mais en fait c’est un fonctionnement vraiment logique.

Il faut aussi savoir qu’un conteneur est un widget. Ça signifie que l’on peut mettre un conteneur dans un conteneur sans aucun problème. Et c’est là que résulte toute la puissance de ce système.

Un grand pouvoir implique de grandes responsabilités. En effet, les widgets sont un système vraiment efficace. Cependant, si vous souhaitez les emboîter, votre architecture risque de devenir très complexe et vous allez vous perdre. Je vous conseil donc deux solutions :

  • Dessiner sur une feuille votre application afin d’avoir un modèle
  • Attendre patiemment la partie de ce tutoriel à propos de Glade :-°

Nous allons utiliser deux types de conteneurs, les autres étant spécifiques à certaines utilisations. Le type Gtk.Box et le type Gtk.Grid.

Les boîtes

Le type Gtk.Box est sûrement le conteneur le plus simple qu’il soit. Il vous permet de de créer des lignes ou des colonnes de widgets. Un Gtk.Box équivaut à une ligne ou une colonne. Voyez l’exemple ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from gi.repository import Gtk


# On créer notre fenêtre, comme d'habitude
window = Gtk.Window()
window.set_border_width(10)  # Je met une bordure pour que ce soit visuellement plus joli
window.connect('delete-event', Gtk.main_quit)

# On créer un conteneur de type Gtk.Box()
box = Gtk.Box(spacing=6)  # L'argument "spacing" équivaut à l'écartement entre les widgets

# On créer deux boutons totalement bidons
button_1 = Gtk.Button(label='Je suis le bouton n°1')
button_2 = Gtk.Button(label='Je suis le bouton n°2')

# Et le plus important, on "pack" les boutons dans notre box
# c'est-à-dire qu'on les ajoute dedans
# Le bouton 1 :
box.pack_start(button_1, True, True, 0)
# Le bouton 2 :
box.pack_start(button_2, True, True, 0)

# Et enfin on ajoute ce conteneur à notre fenêtre
window.add(box)

window.show_all()
Gtk.main()

Voici le résultat :

Ici, pas mal de nouveauté. Tout d’abord, je créer mon conteneur avec :

1
box = Gtk.Box(spacing=6)

box utilise le type par défaut de conteneur, c’est à dire qu’il sera horizontal. De plus, tous ces enfants seront espacés de 6 pixels, mais attention, juste à droite et à gauche ! Comme le conteneur est horizontal, l’espacement se fait horizontalement. L’inverse se produit quand le conteneur est vertical.

Je créer ensuite mes deux boutons en leur assignant un label, rien de nouveau pour vous (du moins, je l’espère :p ).

L’action que je fais ensuite s’appelle packer. C’est à dire que j’ajoute, j’assigne mes boutons à mon conteneur. Je pack mes boutons. Pour ça, j’utilise la méthode Gtk.Box.pack_start() :

1
2
3
4
# Le bouton 1 :
box.pack_start(button_1, True, True, 0)
# Le bouton 2 :
box.pack_start(button_2, True, True, 0)

Cette méthode ajoute, de gauche à droite, vos widgets. Elle prend 4 arguments, tous obligatoires. Le premier est simplement le widget à ajouter. Un bouton, une image, un autre conteneur… Tout ce que vous voulez qui soit un widget.

Le deuxième est booléen. Il permet que si plus d’espace est disponible, d’autoriser votre conteneur à diviser cet espace en part égale pour chaque widget. Par exemple, si j’agrandis la fenêtre, plus d’espace sera disponible, donc nous verrons le widget s’agrandir. Si cet argument est sur False, alors cette case ne sera pas agrandie. Essayer d’agrandir la fenêtre avec cette valeur à False. Pour les plus flemmards d’entre vous, petite démonstration avec un gif :

Le bouton 1 a son redimensionnement réglé sur False tandis que celui du bouton 2 est réglé sur True

Nous travaillons dans un conteneur horizontal, cette propriété n’affecte que l’axe horizontal. Il est donc parfaitement normal que le widget voit sa hauteur modifiée.

Le troisième argument y ressemble un peu. C’est encore un booléen sauf que là l’espace disponible pour le widget va s’agrandir sans que celui-ci (le widget) ne cherche à le remplir. Pour bien comprendre, il vous faut absolument tester par vous même et tant pis pour les plus flemmards, pas de gif cette fois. :diable: Il faut, en tout logique, que le second argument soit sur vrai (pour autoriser la box à agrandir la case mais pour interdire le widget à remplir cette case).

Et enfin, le dernier argument est un entier. Il correspond à la taille des marges à gauche et à droite de ce widget. Il faut que cette taille soit supérieur à l’espacement entre les widgets pour en voir les effets.

Sachez qu’il existe la même méthode mais qui ajoute les widgets de droite à gauche, Gtk.Box.pack_end().

Ici nous avions un conteneur horizontal. Je vous présente le même, mais vertical :

Et voici comment faire :

1
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)

Par défaut c’est horizontal, mais si pour une raison que je ne connais pas, vous avez besoin de le préciser :

1
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)

Et voilà, vous savez tout à propos des conteneurs de type boîte. Nous allons voir le conteneur de type Grille, qui est plus simple à comprendre je trouve et beaucoup plus complet. Par contre, je le trouve moins précis.

Les grilles

Les grilles seront sûrement un des widgets que vous utiliserez le plus au début. En effet, elles sont plus simple à manipuler et permettent beaucoup plus de chose. Seulement, au moment ou vous commencerez à faire des fenêtre avec beaucoup de widgets vous découvrirez leur principal défaut.

Voyez les grilles comme un tableau de LibreOffice Calc ou Excel. C’est à dire des cases qui appartiennent à des lignes et des colonnes. Vous allez attacher vos widgets à ces cases et vous allez pouvoir leur indiquer de prendre 3 case de large, 6 cases de haut si vous voulez.

Voici un exemple d’utilisation des grilles :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
# coding: utf-8

from gi.repository import Gtk

window = Gtk.Window()

window.set_title('Demo des grilles')
window.set_border_width(10)

window.connect('delete-event', Gtk.main_quit)

grid = Gtk.Grid()

button_1 = Gtk.Button(label='Je suis le bouton n°1')
button_2 = Gtk.Button(label='Je suis le bouton n°2')
button_3 = Gtk.Button(label='Je suis le bouton n°3')
button_4 = Gtk.Button(label='Je suis le bouton n°4')
button_5 = Gtk.Button(label='Je suis le bouton n°5')

grid.attach(button_1, 0, 0, 3, 1)  # Le bouton_1 se trouve en (0;0), prend 3 cases de large et une de haut
grid.attach(button_2, 0, 1, 1, 3)  # Le bouton_2 se trouve en (0;1), prend une case de large et 3 de haut
grid.attach(button_4, 1, 1, 1, 1)  # Le bouton_4 se trouve en (1;1), prend une case de large et une de haut
grid.attach(button_3, 2, 1, 1, 1)  # Le bouton_3 se trouve en (2;1), prend une case de large et une de haut
grid.attach(button_5, 1, 3, 2, 1)  # Le bouton_5 se trouve en (1;3), prend 2 cases de large et une de haut

window.add(grid)

window.show_all()
Gtk.main()

Et le résultat en image :

Premièrement, il faut créer la grille :

1
grid = Gtk.Grid()

Rien de nouveau. Ce qui change, c’est comment vous allez attacher vos widgets à cette grille. En effet, on ne pack pas ses widgets, on les attache.

1
grid.attach(widget, x, y, size_x, size_y)

La méthode Gtk.Grid.attach() prend 5 arguments obligatoires. Le premier est le widget que vous souhaitez ajouter. Le second et le troisième sont les coordonnées de ce widget. Respectivement, les abscisses et les ordonnées.

Coordonnées d’une Gtk.Grid

Les deux derniers , x_size et y_size représentent le nombre de case que votre widget prendra. Faite attention de ne pas mettre cette valeur à 0 sinon votre widget n’apparaîtra pas et vous aurez un avertissement :

1
Gtk-CRITICAL **: gtk_grid_attach: assertion 'height > 0' failed

De plus, faite aussi attention à ce que vous ne recouvriez pas les autres widgets. Je pense que le meilleur moyen de maîtriser ce conteneur est de pratiquer, non ? ;)

Mais avant de vous laisser vous débrouiller, j’aimerais vous présentez quatre nouvelles méthodes.

Tout d’abord, Gtk.Grid.set_column_homogeneous() et Gtk.Grid.set_row_homogeneous(). Si vous avez essayé de faire une grille, vous avez peut-être remarqué que quand vous agrandissiez la fenêtre, la grille (et donc les widgets contenus dedans) ne suivait pas. En effet, votre grille n’est, par défaut, pas homogène.

Gtk.Grid.set_column_homogeneous() prend en argument un booléen et permet d’autoriser ou non les colonnes de la grille à "suivre" le mouvement de la fenêtre. Pour simplifier, ça signifie que vos widgets contenus dans cette grille vont s’agrandir horizontalement.

Gtk.Grid.set_row_homogeneous(), comme sa sœur, prend en argument un booléen. Sauf qu’ici, ce sont les lignes qui vont avoir le droit de s’agrandir ou non.

Les deux derniers vont aussi ensembles. Grâce à Gtk.Grid.set_row_spacing() et Gtk.Grid.set_column_spacing()` vous allez pouvoir définir l’espace entre chacun de vos widgets verticalement ou horizontalement. Je vous laisse jouer avec pour en découvrir les effets.

Un compteur de clics

Allez on est parti !

Je voudrais (et oui, j’exige maintenant :-° ) que vous me fassiez une fenêtre qui compte le nombre de clics sur un bouton. Voici ce que j’attends :

Résultat de l’exercice, une fenêtre qui compte le nombre de clique !

Si vous réussissez ce défi, c’est que vous avez tout compris jusque là. Si vous n’y arrivez pas, ne vous précipitez pas sur la solution, allez relire les chapitres où vous bloquez. Et si ce qui vous pose problème est ce compteur et non l’interface, voici un indice :

1
global mon_compteur

Ma solution (parce que vous avez sûrement trouvé une autre façon de faire !) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python3
# coding: utf-8

from gi.repository import Gtk


def build_interface():
   '''
   Fonction qui retourne le layout contenant toute notre interface
   '''
   grid = Gtk.Grid()

   grid.set_row_spacing(6)
   grid.set_column_spacing(6)

   click_counter_label = Gtk.Label('Vous n\'avez pas encore cliqué sur le bouton.')
   grid.attach(click_counter_label, 1, 0, 1, 1)

   add_counter_btn = Gtk.Button(label='Cliquez ici !')
   add_counter_btn.connect('clicked', on_add_counter, click_counter_label)
   grid.attach(add_counter_btn, 0, 0, 1, 1)

   quit_btn = Gtk.Button(label='Quitter')
   quit_btn.connect('clicked', Gtk.main_quit)
   grid.attach(quit_btn, 1, 1, 1, 1)

   return grid


def on_add_counter(button, label):
   '''
   Quand l'utilisateur clique sur le bouton, ajoute +1 au compteur et change le
   label
   '''
   global click_counter  # On récupère le compteur
   click_counter += 1
   # On change le label avec le nouevau texte
   label.set_text('Vous avez cliqué {} fois sur le boutton !'.format(click_counter))


if __name__ == '__main__':
   window = Gtk.Window()

   window.set_title('Exercice n°1')
   window.set_border_width(10)
   window.connect('delete-event', Gtk.main_quit)

   click_counter = 0  # Notre compteur de clics

   # On récupère le layout et on l'ajoute à la fenêtre
   grid = build_interface()
   window.add(grid)

   window.show_all()
   Gtk.main()

Ceux qui, comme moi, on utilisé une grille on le droit à un bouton Quitter énorme. En effet, le label au-dessus étant plus large, la colonne s’agrandit en conséquence. Et le bouton quitter également. Plusieurs solutions pour résoudre ce problème :

  • Interdire le bouton de remplir l’espace en réglant son alignement horizontal sur centré (par défaut il remplit tout l’espace) avec button.set_halign(Gtk.Align.CENTER)
  • Utiliser des Gtk.Box (solution que je préfère)
  • Déplacer le bouton

Autre problème, c’est cette variable globale. Ce problème sera résolu grâce à la POO que nous verrons plus tard.


Vous avez désormais vu le plus gros de GTK+. Dans le prochain chapitre, je vais vous présenter quelques widgets que je trouve utile ou important. Il ne vous manque plus que la POO pour pouvoir commencer à faire des vraies fenêtres !