← Retour aux articles

Python : Tkinter et le layout Pack

Comprendre le layout Pack dans les interfaces Tkinter

1) A propos des Geometry Manager

Tkinter contient trois types de gestionnaire (= Geometry Manager) pour positionner les éléments d'une interface graphique. Je les nomme : il s'agit de Grid, Pack et Place.

Contrairement à ce qu'on peut lire dans de nombreux forums sur Python, il est tout à fait permis d'utiliser PLUSIEURS gestionnaires de positionnement différent dans une même application avec Tkinter. Sans ça, je ne vois d'ailleurs pas comment on pourrait faire des interfaces plus ou moins sophistiquées... C'est tout bonnement impossible.

C'est seulement au sein d'un même élément "conteneur" (c-à-d capable de recevoir des éléments : boutons, listbox, etc...), qu'il faut rester impérativement dans le Geometry Manager choisi. Donc, on peut tout à fait avoir un conteneur utilisant Grid() pour positionner ses propres enfants directs, disposé juste à côté d'un conteneur utilisant Pack() pour ses enfants à lui, et cela au sein de la même application. C'est ce que je vais illustrer dans ce didacticiel par un exemple très simple, et détaillé pas à pas.

Le conteneur par excellence est l'élément Frame().

2) Comment fonctionne réellement Pack ?

Dans ce didacticiel, je vais surtout vous montrer comment fonctionne Pack(), qui m'a un peu donné mal à la tête lors de mes premières tentatives pour le comprendre et l'utiliser correctement. Si j'avais eu connaissance autrefois des explications qui vont suivre, j'aurais gagné beaucoup de temps dans la création et la conception des interfaces de mes programmes.

Pack fonctionne en réalité en utilisant ce que j'appelerais "la cavité restante". Lançons-nous dans notre exemple simple.

#!/usr/local/bin/python
# -*- coding:utf-8 -*-
from Tkinter import *

application = Tk()
application.title("Pack testeur")

frame1 = Frame(application, bg="yellow", width=300, height=300, padx=10, pady=10)
frame1.pack(side=RIGHT,fill=Y)

# il reste la cavité sur la gauche...

application.mainloop()

Nous obtenons une fenêtre qui affiche un frame carré de 300 pixels de côté. Normal ! Nous avons spécifié les dimensions voulues de ce Frame lors de sa création à la ligne 08. Grâce au schéma ci-dessous, je peux commencer à vous expliquer clairement ce qui se passe.

Nous avons créé le frame1, et nous lui avons demandé de se mettre dans l'espace libre situé à droite (side=RIGHT) de sa fenêtre parente, càd application = Tk(). Ensuite, nous avons spécifié qu'il doit remplir l'espace qui lui a été attribué en s'étalant suivant l'axe des Y. C'est ce qui est symbolisé par les deux flèches rouges dans le schéma ci-dessus.

Lorsqu'on fait appel à pack(), Tkinter compacte la fenêtre pour épouser au mieux la forme des éléments qu'elle contient. Comme il n'y a qu'un frame présent, la fenêtre générale épouse la forme de frame1. Maintenant, vous pouvez faire le test intéressant d'agrandir la fenêtre (exemple en la maximisant), vous verrez que Tkinter a bien placé frame1 dans une zone à droite, et qu'il l'étend suivant l'axe des Y, comme expliqué ci-dessus.

Il faut maintenant considérer la cavité restante. Il faut bien comprendre que le prochain ajout d'un widget dans la fenêtre application se fera par rapport à cette cavité restante. C'est comme si on pouvait "oublier" qu'on a positionné frame1 à droite. Seul compte maintenant pour la suite la surface (limitée en pointillés dans le schéma) représentée par la cavité restante.

Principe général :

En fait, avec Pack(), chaque futur ajout d'un widget (que ce soit un conteneur ou non) se fera suivant une des 4 options illustrées ci-dessous. On "tranche" en quelque sorte la cavité restante, pour donner naissance à une nouvelle cavité restante, qui subira à nouveau le même schéma au prochain ajout d'un élément.

Il faut aussi noter que les zones situées dans la tranche de part et d'autre du widget (cf zones en vert dans le schéma ci-dessus) peuvent rester inutilisées ou non, en fonction de l'expansion spécifiée au widget nouvellement inséré (cf options fill et expand).

Ajoutons maintenant notre deuxième frame :

#!/usr/local/bin/python
# -*- coding:utf-8 -*-
from Tkinter import *

application = Tk()
application.title("Pack testeur")

frame1 = Frame(application, bg="yellow", width=300, height=300, padx=10, pady=10)
frame1.pack(side=RIGHT,fill=Y)

# il reste la cavité sur la gauche...

frame2 = Frame(application, bg="blue",   width=300, height=150)
frame2.pack(side=TOP, fill=X)

# il reste une cavité en bas à gauche...

application.mainloop()

Le schéma ci-dessous va encore une fois nous aider à éclaircir ce qui s'est passé.

Le nouveau Frame frame2 est allé se placer en haut de la cavité restante (cf side=TOP), et nous lui avons dit qu'il peut remplir tout l'espace suivant l'axe des X (voir les flèches rouges). Le principe est toujours le même, il reste maintenant une nouvelle cavité restante qui est en quelque sorte placée en bas à gauche de la fenêtre générale application.

C'est cette "nouvelle" cavité restante qu'il faut maintenant considérer par rapport à l'ajout du prochain élément direct de notre fenêtre principale application = Tk(). Ajoutons donc le 3ème frame :

#!/usr/local/bin/python
# -*- coding:utf-8 -*-
from Tkinter import *

application = Tk()
application.title("Pack testeur")

frame1 = Frame(application, bg="yellow", width=300, height=300, padx=10, pady=10)
frame1.pack(side=RIGHT,fill=Y)

# il reste la cavité sur la gauche...

frame2 = Frame(application, bg="blue",   width=300, height=150)
frame2.pack(side=TOP, fill=X)

# il reste une cavité en bas à gauche...

frame3 = Frame(application, bg="red",    width=300, height=150)
frame3.pack(side=BOTTOM, fill=BOTH, expand=1)

application.mainloop()

Et nous obtenons visuellement ceci :

Le frame3 est allé se placer en bas de la cavité. Mais si nous maximisons la fenêtre, nous voyons que ce frame3 rouge prend dorénavant tout l'espace restant. Pourquoi ? Rien ne vaut encore un schéma supplémentaire pour bien comprendre :

Nous avons en effet donné à frame3 les caractéristiques suivantes : fill=BOTH et expand=1. Ce qui veut dire que nous donnons à ce frame l'autorisation de remplir SA cavité dans les deux sens X et Y, et nous lui donnons aussi l'injonction de s'étendre impérativement avec expand=1 (ce qui est possible seulement si la cavité restante ne contient pas de widget en fait).

Donc, dans ce cas, la cavité restante est TOUJOURS présente, mais est sans cesse réduite à une surface nulle, car le frame3 cherche à s'étendre tant que c'est possible !

Continuons notre investigation en ajoutant encore un 4ème frame : frame4 pour terminer notre initiation à pack(). On peut déjà prédire que, quelle que soit l'option side que nous donnerons à frame4, celui-ci sera OBLIGATOIREMENT pris en sandwish entre frame2 et frame3, par le fait que la cavité restante est ainsi positionnée.

#!/usr/local/bin/python
# -*- coding:utf-8 -*-
from Tkinter import *

application = Tk()
application.title("Pack testeur")

frame1 = Frame(application, bg="yellow", width=300, height=300, padx=10, pady=10)
frame1.pack(side=RIGHT,fill=Y)

# il reste la cavité sur la gauche...

frame2 = Frame(application, bg="blue",   width=300, height=150)
frame2.pack(side=TOP, fill=X)

# il reste une cavité en bas à gauche...

frame3 = Frame(application, bg="red",    width=300, height=150)
frame3.pack(side=BOTTOM, fill=BOTH, expand=1)

# le frame suivant sera placé au-dessus du frame 3 !

frame4 = Frame(application, bg="green",    width=200, height=100)
frame4.pack(fill=NONE, expand=1)

application.mainloop()

Nous obtenons ceci (avec une fenêtre que j'ai légèrement agrandie suivant l'axe des Y) :

Nous avons dit à frame4 de ne remplir aucune direction (fill=NONE) mais de quand même s'étendre (expand=1). Comme nous n'avons rien spécifié pour side, c'est par défaut l'option side=TOP qui est appliquée. De plus, frame4 a ses propres dimensions bien définies (200 et 100), il n'y a donc aucune raison que le frame4 de couleur verte s'étende quand on maximise la fenêtre principale. Voyons le dernier schéma pour bien comprendre :

En fait, dans ce cas, c'est la cavité contenant frame4 qui va s'étendre mais pas le frame4. La cavité restante est donc ici de nouveau réduite à sa superficie minimale, puisque la cavité précédente s'étend au plus possible.

3) Caractéristique importante de pack

Par rapport à ce qui a été exposé ci-dessus, vous pouvez déjà en déduire une caractéristique importante du Geometry Manager pack() : l'ordre dans lequel sont ajoutés les widgets à un même conteneur-parent a une grande importance.

4) Et on peut aller plus loin !

Une erreur fréquemment trouvée sur les forums consacrés à la programmation avec Tkinter consiste à dire que l'on ne peut utiliser qu'un seul Geometry Manager au sein d'une même application. C'est faux.

La règle n'est donc pas celle-ci :
- "Vous ne pouvez utiliser qu'un seul Geometry Manager dans une application."
Mais elle est la suivante :
- "Vous ne pouvez utiliser qu'un seul Geometry Manager dans un conteneur donné pour y placer ses enfants directs."

Illustrons cela en complétant notre exemple. Si on se concentre maintenant sur notre frame1, je peux décider d'y placer dedans des widgets (enfants directs) en utilisant cette fois-ci le Geometry Manager grid. La règle stipule donc qu'une fois que vous avez choisi Grid pour un conteneur donné, tous les enfants directs de ce conteneur devront être placés avec Grid.

#!/usr/local/bin/python
# -*- coding:utf-8 -*-
from Tkinter import *

application = Tk()
application.title("Pack testeur")

frame1 = Frame(application, bg="yellow", width=300, height=300, padx=10, pady=10)
frame1.pack(side=RIGHT,fill=Y)

# il reste la cavité sur la gauche...

frame2 = Frame(application, bg="blue",   width=300, height=150)
frame2.pack(side=TOP, fill=X)

# il reste une cavité en bas à gauche...

frame3 = Frame(application, bg="red",    width=300, height=150)
frame3.pack(side=BOTTOM, fill=BOTH, expand=1)

# le frame suivant sera placé au-dessus du frame 3 !

frame4 = Frame(application, bg="green",    width=200, height=100)
frame4.pack(side=TOP, fill=NONE, expand=1)

# A l'intérieur du frame 1, si on décide d'utiliser grid, on peut le
# faire, mais on utilisera alors exclusivement grid pour les enfants
# directs placés dans frame1.

lab1 = Label(frame1, text="label un"   ).grid(row=0,column=0)
ent1 = Entry(frame1).grid(row=0,column=1)
lab2 = Label(frame1, text="label deux" ).grid(row=1,column=0)
ent2 = Entry(frame1).grid(row=1,column=1)
lab3 = Label(frame1, text="label trois").grid(row=2,column=0)
ent3 = Entry(frame1).grid(row=2,column=1)

application.mainloop()

Ce qui donne l'interface mixte suivante :

5) Conclusion générale

C'est en jouant sur le remplissage de la notion de cavité restante, et en imbriquant des conteneurs intelligemment, tout en respectant la règle de rester dans un Geometry Manager donné pour chaque conteneur utilisé, que vous pourrez élaborer avec Tkinter des interfaces suffisemment complexes, et qui réagiront comme vous l'aurez planifié à la modification de la fenêtre principale par l'utilisateur.

Voilà, j'espère que ce didacticiel aura permis aux débutants qui utilisent Tkinter de mieux comprendre Pack() et ainsi de l'utiliser plus rapidement et plus efficacement, pour constituer leurs interfaces utilisateurs en toute connaissance de cause.

Calogero GIGANTE