CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Le GUI playground

Le GUI playground est l'endroit où vous jouerez avec le code pour apprendre à créer une interface graphique avec tkinter.

Ouvrez dans Visual Studio Code le squelette PistusResa que vous avez téléchargé et modifié dans le TD précédent.

👉 Dans le dossier gui, vous trouverez un fichier playground.py, dans lequel vous pourrez essayer les exemples présentés dans cette partie.

Ouvrir une fenêtre

Afin d'ouvrir une fenêtre avec le titre Playground, copiez et collez le code suivant dans le fichier ./gui/playground.py et exécutez-le :

# Create a window.
window = tk.Tk()

# Give the window a title.
window.title("Playground")

# Print a message in the terminal.
print("Opening the window")

# Trigger the execution of the event loop
window.mainloop()

# Print a message in the console.
print("The window is now closed")

L'instruction window.mainloop() déclenche l'exécution de la boucle d'événements.

Lorsqu'elle est dans la boucle d'événements, l'interface graphique surveille tout événement susceptible d'être déclenché par les actions de l'utilisateur sur les widgets, tels que les clics sur les boutons, les pressions de touches, etc. Il est important de noter que l'appel de la fonction window.mainloop() ne permet pas l'exécution du code qui vient après. Si vous regardez le terminal dans Visual Studio Code, vous devriez lire le message "Opening the windows", alors que le message "The window is now closed" n'apparaît pas. Ce message n'apparaît que lorsque vous fermez la fenêtre, ce qui arrête la boucle.

Ajouter une étiquette à la fenêtre

Le code suivant crée une fenêtre contenant une étiquette qui porte le texte "First label" :

window = tk.Tk()
window.title("Playground")

# Create a label. We pass two arguments:
# * the parent window: where the label is placed.
# * the text of the label.
my_label = tk.Label(window, text="First label")

# Invoke the pack geometry manager
my_label.pack()

window.mainloop()

Observez comment la taille de la fenêtre est calculée sur celle de l'étiquette : c'est l'effet de l'utilisation de la fonction pack.

La fonction pack() invoque le pack geometry manager, responsable du placement des widgets dans une fenêtre. Le GIF suivant montre comment fonctionne le pack geometry manager. Tout d'abord, la fenêtre est créée avec une certaine taille. Chaque fois que nous invoquons la fonction pack sur un widget de cette fenêtre, le geometry manager place le widget dans la partie supérieure de la fenêtre qui n'est pas occupée par un autre widget. Lorsque tous les widgets sont placés, la taille de la fenêtre est calculée pour permettre l'affichage de tous les widgets.


Nous pouvons utiliser l'argument side de la fonction pack pour forcer le gestionnaire de géométrie à ajouter les widgets à gauche, à droite ou en bas de la fenêtre, au lieu de celui du haut.

Un gestionnaire de géométrie alternatif est grid qui est utilisé pour placer les widgets dans une grille.


Remplacez l'instruction :

my_label = tk.Label(window, text="First label")

par l'instruction suivante :

my_label = tk.Label(window, width=10, height=10, text="First label")

Bien que les valeurs de width et height soient identiques, l'étiquette n'est pas carrée. Cela est dû au fait que la taille est spécifiée en unités de texte.

👉 Une unité de texte correspond à la largeur (respectivement, la hauteur) du caractère 0 (le chiffre zéro).

Ce choix garantit que l'apparence du widget est cohérente sur toutes les plateformes. Le widget aura toujours la taille nécessaire pour afficher 10 zéros, à la fois horizontalement et verticalement, indépendamment de la police utilisée.

Autres widgets

Ajoutons d'autres widgets à notre fenêtre : un champ de texte et un bouton.

Copiez le code suivant dans le terrain de jeu et lisez les commentaires.

window = tk.Tk()
window.title("Playground")

my_label = tk.Label(window, text="First label")

# This variable stores the text field current content. It is useful when we want to track what the user types and react to it.
my_text_field_var = tk.StringVar(value="")

# Create a text field, where the user can type some text that fits in one line.
# Note that we pass my_text_field_var so that all we type will be stored in that variable.
my_text_field = tk.Entry(window, textvariable=my_text_field_var)

# Add a button. If you click on the button, nothing happens because we didn't specify 
# any action. 
my_button = tk.Button(window, text="First button")

# Invoke the pack geometry manager on all widgets.
my_label.pack()
my_text_field.pack()
my_button.pack()

window.mainloop()


Notez que la fonction pack() est invoquée pour chaque widget.


Utiliser ttk

Tkinter fournit des widgets réguliers (ceux que nous avons utilisés dans les exemples précédents) qui ont un aspect cohérent sur toutes les plates-formes ; en conséquence, ils ont un aspect un peu primitif, et certainement différent des widgets qui apparaissent dans d'autres applications fonctionnant sur votre système d'exploitation.

Depuis la version 8.5, Tkinter met à disposition un sous-module appelé ttk contenant des widgets thématiques qui sont conçus pour avoir un aspect plus natif, c'est-à-dire similaire aux widgets utilisés dans votre système d'exploitation.


Les widgets ttk sont plus difficiles à styliser (par exemple, changer la police ou la couleur d'arrière-plan) que les widgets ordinaires. Les configurations de style sont déjà définies pour vous dans le fichier ./gui/gui_config.py.


Dans le code suivant, nous appelons la fonction configure_style définie dans le fichier ./gui/gui_config.py. Cette fonction configure l'aspect et le comportemenet des différents widgets utilisés dans l'interface graphique de PistusResa. Nous créons l'étiquette, le champ de texte et le bouton ttk.Label, ttk.Entry et ttk.Button respectivement.

window = tk.Tk()
window.title("Playground")
configure_style()
my_label = ttk.Label(window, text="First label")
my_text_field_var = tk.StringVar(value="")
my_text_field = ttk.Entry(window, textvariable=my_text_field_var)
my_button = ttk.Button(window, text="First button")
my_label.pack()
my_text_field.pack()
my_button.pack()
window.mainloop()

Utilisation de frames

Dans les exemples précédents, nous avons ajouté les widgets directement dans la fenêtre. Cependant, lorsque l'interface graphique se compose de plusieurs widgets, il est utile de les regrouper dans des frames ; les widgets qui ont la même fonction (par exemple, tous les widgets utilisés pour collecter les données personnelles d'un étudiant) sont placés dans le même frame.

Les frames étant indépendants les uns des autres, chaque frame peut utiliser un gestionnaire de géométrie différent.

Dans le code suivant, l'étiquette, le champ de texte et le bouton sont tous placés dans le frame first_frame. Copiez le code dans le playground, lisez les commentaires et exécutez-le. Notez comment le frame et l'étiquette sont stylisés.

window = tk.Tk()
window.title("Playground")
configure_style()

# We create a frame and set the style.
# Sample.TFrame is defined in file gui_config.py. 
# It sets the background color of the frame to yellow.
first_frame = ttk.Frame(window, style="Sample.TFrame")

# We create a label and set the style.
# Sample.TLabel is defined in file gui_config.py.
# It sets the background color of the label to red.
first_label = ttk.Label(first_frame, text="First label", \
                        style="Sample.TLabel")


first_text_field_var = tk.StringVar(value="")

first_text_field = ttk.Entry(first_frame, \
                             textvariable=first_text_field_var)
first_button = ttk.Button(first_frame, text="First button")

# The call to pack() places these widgets into their parent 
# frame (first_frame)
first_label.pack()
first_text_field.pack()
first_button.pack()

# The call to pack() places the first_frame into its parent 
# window.
first_frame.pack()

window.mainloop()

Plus d'informations sur le gestionnaire de géométrie pack

Si vous essayez de redimensionner la fenêtre, le frame (la partie jaune) se colle à la partie supérieure de la fenêtre. C'est le comportement par défaut du gestionnaire de géométrie pack. Vous pouvez le modifier en donnant au paramètre side de la fonction pack() l'une des valeurs suivantes : tk.LEFT, tk.RIGHT et tk.BOTTOM.

👉 Essayez différentes valeurs pour voir leur effet.

Vous avez certainement remarqué que le frame ne s'étend pas lors du redimensionnement de la fenêtre. Nous pouvons utiliser les paramètres fill et expand pour activer ce comportement.

Les valeurs possibles de fill sont :

  • tk.X. Le frame remplira horizontalement la fenêtre.
  • tx.Y. Le frame remplit la fenêtre verticalement.
  • tk.BOTH. Le cadre remplira la fenêtre à la fois horizontalement et verticalement.

Les valeurs possibles de expand sont :

  • True. Le frame s'étend pendant que la fenêtre est redimensionnée.
  • False. Le frame ne s'agrandit pas.


Modifiez le code pour que l'étiquette remplisse le frame d'appartenance (même lorsque le frame est redimensionné). Exécutez le code et essayez de redimensionner la fenêtre.

Prenons un autre exemple pour mieux comprendre le fonctionnement du gestionnaire de géométrie pack.

window = tk.Tk()
window.title("Playground")
configure_style()
first_frame = ttk.Frame(window, style="Sample.TFrame")
first_label = ttk.Label(first_frame, text="First label",\
                         style="Sample.TLabel")
first_ent_var = tk.StringVar(value="")
first_text_field = ttk.Entry(first_frame,\
                             textvariable=first_ent_var)
first_button = ttk.Button(first_frame, text="First button")
first_label.pack(side=tk.LEFT)
first_text_field.pack()
first_button.pack()
first_frame.pack()
window.mainloop()


Expliquez la position des widgets dans la fenêtre.


Le gestionnaire de géométrie grid

Le gestionnaire de géométrie pack est très puissant, mais il est difficile à utiliser. Un gestionnaire de géométrie tout aussi puissant, mais plus intuitif, est grid.

Essayez le code suivant.

window = tk.Tk()
window.title("Playground")
configure_style()
first_frm = ttk.Frame(window)
first_lbl = ttk.Label(first_frm, text="Grid (0, 0)",\
                      style="Sample.TLabel")
second_lbl = ttk.Label(first_frm, text="Grid (0, 1)",\
                       style="SampleTwo.TLabel")
third_lbl = ttk.Label(first_frm, text="Grid (1, 0)",\
                      style="SampleThree.TLabel")
fourth_lbl = ttk.Label(first_frm, text="Grid (1, 1)", style="SampleFour.TLabel")
first_lbl.grid(row=0, column=0)
second_lbl.grid(row=0, column=1)
third_lbl.grid(row=1, column=0)
fourth_lbl.grid(row=1, column=1)
first_frm.pack()
window.mainloop()

Ce code crée quatre étiquettes (avec des couleurs de fond différentes) et les dispose dans une grille de deux lignes et deux colonnes (notez l'utilisation de la fonction grid). Les lignes et les colonnes sont identifiées par un indice ; les deux indices commencent à 0.

Dans la fenêtre créée par le code précédent, il n'y a pas d'espace entre les étiquettes, ce qui rend l'interface graphique un peu encombrée. Nous pouvons libérer de l'espace autour des étiquettes en utilisant les arguments padx et pady de la fonction grid, comme suit :

first_lbl.grid(row=0, column=0, padx=10, pady=10)
second_lbl.grid(row=0, column=1, padx=10, pady=10)
third_lbl.grid(row=1, column=0, padx=10, pady=10)
fourth_lbl.grid(row=1, column=1, padx=10, pady=10)

Utilisation de deux frames

Dans l'exemple suivant, nous ajoutons deux frames à la fenêtre ; chaque frame utilise le gestionnaire de géométrie grid, mais dans le frame supérieur (le jaune), les étiquettes sont disposées dans une grille de 4x4, tandis que dans le frame inférieur (le bleu), les étiquettes sont disposées dans une grille de 1x3.

window = tk.Tk()
window.title("Playground")
configure_style()

first_frm = ttk.Frame(window, style="Sample.TFrame")
ttk.Label(first_frm, text="Grid (0, 0)", style="Sample.TLabel")\
    .grid(row=0, column=0, padx=10, pady=10)
ttk.Label(first_frm, text="Grid (0, 1)", style="Sample.TLabel")\
    .grid(row=0, column=1, padx=10, pady=10)
ttk.Label(first_frm, text="Grid (1, 0)", style="Sample.TLabel")\
    .grid(row=1, column=0, padx=10, pady=10)
ttk.Label(first_frm, text="Grid (1, 1)", style="Sample.TLabel")\
    .grid(row=1, column=1, padx=10, pady=10)
first_frm.pack()

second_frm = ttk.Frame(window, style="SampleBottom.TFrame")
ttk.Label(second_frm, text="Grid (0, 0)", style="Sample.TLabel")\
    .grid(row=0, column=0, padx=10, pady=10)
ttk.Label(second_frm, text="Grid (0, 1)", style="Sample.TLabel")\
    .grid(row=0, column=1, padx=10, pady=10)
ttk.Label(second_frm, text="Grid (0, 2)", style="Sample.TLabel")\
    .grid(row=0, column=2, padx=10, pady=10)
second_frm.pack()

window.mainloop()

Dans la fenêtre produite par le code précédent, le frame supérieur ne remplit pas la fenêtre. Modifiez le code pour qu'il en soit ainsi. Exécutez le programme.


columnconfigure and rowconfigure

Dans la fenêtre produite par le code que vous avez modifié, les étiquettes du frame supérieur sont collées sur le côté gauche du frame.

Si nous voulons qu'elles soient disposées uniformément sur tout le frame, nous devons appeler la fonction columnconfigure() sur le frame supérieur, comme suit (placez ces deux lignes juste avant l'instruction que vous avez modifiée à la question précédente) :

first_frm.columnconfigure(0, weight=1)
first_frm.columnconfigure(1, weight=1)

👉 La fonction columnconfigure() prend deux arguments :

  • l'index de la colonne à configurer ;
  • le weight qui définit le taux d'expansion des widgets (typiquement, on utilise 1) lorsque le cadre est redimensionné. Vous pouvez essayer différentes valeurs de weight (pas nécessairement la même valeur pour les deux colonnes) et redimensionner la fenêtre pour en voir l'effet.

Il existe une fonction équivalente, rowconfigure().

Alignement d'un widget

Nous remarquons que toutes les étiquettes sont placées au centre de leurs cellules respectives. Nous pouvons modifier ce comportement en utilisant l'argument sticky de la fonction grid(). Les valeurs possibles de sticky sont :

  • 'w'. Alignement ouest (ou gauche) ;
  • 'e'. Alignement est (ou droite) ;
  • 'n'. Alignement nord (ou supérieur) ;
  • 's'. Alignement sud (ou inférieur) ;
  • toute concaténation de ces valeurs (par exemple, 'ew').

Par exemple, modifiez l'emplacement de l'étiquette à (0, 0) du frame supérieur avec le code suivant :

ttk.Label(first_frm, text="Grid (0, 0)", style="Sample.TLabel")\
    .grid(row=0, column=0, padx=10, pady=10, sticky='ew')

Comme nous utilisons la valeur 'ew' pour l'argument sticky, l'étiquette s'étend horizontalement sur toute la cellule. Vous pouvez jouer avec différentes valeurs pour voir ce que vous obtenez.

Gestion des événements

Les interfaces que nous avons créées jusqu'à présent ne réagissent pas aux actions de l'utilisateur. Chaque action (par exemple, taper du texte dans un champ de texte, cliquer sur un bouton) génère un événement. Pour chaque événement, nous devons écrire le code à exécuter en réponse à cet événement ; en d'autres termes, nous associons à chaque événement une fonction (event handler ou callback) qui est appelée lorsque l'événement se produit.

Essayez le code suivant.

# Callback associated to the button.
def click_ok_handler():
    print("The user clicked OK")

window = tk.Tk()
window.title("Playground")
configure_style()
first_frm = ttk.Frame(window, style="Tab.TFrame")

# Associate the button to the callback click_ok_handler() 
# with the parameter command
button = ttk.Button(first_frm, text="OK", command=click_ok_handler)
button.pack()
first_frm.pack()
window.mainloop()

Ce code :

  • définit une fonction click_ok_handler() qui imprime le message "The user clicked OK" dans le terminal.
  • crée un frame et ajoute un bouton.
  • utilise le paramètre command de ttk.Button pour associer la fonction click_ok_handler() au bouton. Cela signifie qu'à chaque fois que l'utilisateur clique sur le bouton OK, la fonction click_ok_handler() est invoquée.

Exécutez le code et essayez par vous-même. Chaque fois que vous cliquez sur le bouton OK, le message "The user clicked OK" doit apparaître dans le terminal de Visual Studio Code.

Un exemple plus complet

L'interface créée par le code suivant se compose d'un champ de texte et d'un bouton. Initialement, le bouton est désactivé. Lorsque vous tapez quelque chose dans le champ de texte, son contenu est imprimé dans le terminal de Visual Studio Code ; le bouton n'est activé que si le dernier caractère du champ de texte est "#".

Copiez ce code et collez-le dans Visual Studio Code. Lisez les commentaires pour comprendre le code.

# The text field
text_field = None

# The OK button
ok_button = None

# Contains the current content of the text field
text_field_content = None

# Callback associated to the OK button
# (button defined below)
def click_ok_handler():
    print("The user clicked OK")

# Callback associated to the variable text_field_content 
def check_content(*args):

    # With get(), we obtain the string contained 
    # in variable text_field_content.
    # strip() removes leading and trailing whitespaces
    content = text_field_content.get().strip()

    print("The user typed {}".format(content) )

    if content[-1] == "#":
        # Enable the button
        ok_button.configure(state=["!disabled"])
        print("The button is now enabled")
    else:
        # Disable the button (the button will be greyed out)
        ok_button.configure(state=["disabled"])
        print("The button is now disabled")

window = tk.Tk()
window.title("Playground")
configure_style()
frm = ttk.Frame(window, style="Tab.TFrame")

# Initialize the text field current content
text_field_content = tk.StringVar(value="")

# Create text field and associate text_field_content to it
text_field = ttk.Entry(frm, textvariable=text_field_content)

# 
# With function trace(), we say that the function 
# check_content() is called 
# each time the string stored in text_field_content changes
# ("w" indicates a "write" operation on the text field).
#  
text_field_content.trace("w", check_content)

text_field.pack()

# Create the OK button. With the parameter command we 
# say that function click_ok_handler() is invoked when 
# the button is clicked.
ok_button = ttk.Button(frm, state="disabled", text="OK",\
                    command=click_ok_handler)

ok_button.pack()

frm.pack()

window.mainloop()


Exécutez le code et essayez l'interface.

  • Écrivez quelque chose dans le champ de texte, puis supprimez-le. Regardez la sortie dans le terminal de Visual Studio Code. Que se passe-t-il ?
  • Modifiez le code pour résoudre le problème.


Boutons radio

Un bouton radio est un widget qui permet aux utilisateurs de choisir une option parmi plusieurs qui s'excluent mutuellement.

Le code suivant crée deux boutons radio, "Enable" et "Disable" ; lorsque le premier est sélectionné, le bouton OK est activé ; lorsque le second est sélectionné, le bouton OK est désactivé.

👉 Un seul bouton radio peut être sélectionné à la fois.

👉 Copiez le code suivant dans Visual Studio Code, lisez les commentaires pour comprendre comment utiliser les boutons radio et exécutez-le.

# Radio button "enable"
enable_rb = None

# Radio button "disable"
disable_rb = None

# The currently selected radio button 
rb_value = None

# The value assigned to rb_value when
# the radio button "enable" is selected
ENABLED_VAL = 'E'

# The value assigned to rb_value when
# the radio button "disable" is selected
DISABLED_VAL = 'D'

# The OK button
ok_button = None

# Callback associated to the OK button
def click_ok_handler():
    print("The user clicked OK")

# Callback associated to rb_value
def rb_selected(*args):
    current_value = rb_value.get()
    print(current_value)
    if current_value == ENABLED_VAL:
        ok_button.configure(state=["!disabled"])
        print("The OK button is now enabled")
    elif current_value == DISABLED_VAL:
        ok_button.configure(state=["disabled"])
        print("The button is now disabled")

window = tk.Tk()
window.title("Playground")
configure_style()
frm = ttk.Frame(window, style="Tab.TFrame")

# Initially, no radio button is enabled
rb_value = tk.StringVar(value="")

# Trace any change to rb_value.
# When a change occurs, call function rb_selected()
rb_value.trace("w", rb_selected)

# Create the radio button "Enable".
# Associate the value ENABLED_VAL to the radio button
enable_rb = ttk.Radiobutton(frm, text='Enable', value=ENABLED_VAL,\
                             variable=rb_value)

# Create the radio button "Disable".
# Associate the value DISABLED_VAL to the radio button
disable_rb = ttk.Radiobutton(frm, text='Disable', value=DISABLED_VAL,
                              variable=rb_value)

enable_rb.pack()

disable_rb.pack()

# Create the OK button and associate the callback click_ok_handler()
ok_button = ttk.Button(frm, state="disabled", text="OK", \
                       command=click_ok_handler)

ok_button.pack()

frm.pack()

window.mainloop()

Boîtes combinées

Une boîte combinée (anglais, combobox) est un widget qui permet aux utilisateurs de sélectionner une option parmi un ensemble de possibilités.

Le code suivant crée une interface avec trois widgets :

  • Une boîte combo avec quatre choix : "red", "green", "blue", "yellow".
  • Une étiquette, où s'affiche un texte décrivant l'option sélectionnée.
  • Un bouton permettant de fermer la fenêtre.

Copiez le code dans Visual Studio Code, lisez les commentaires et exécutez-le.


# The combo box
color_cb = None

# The destroy button
destroy_button = None

# The selection label
selection_lbl = None

# The main window
window = None

# Callback associated to the combo box
def combo_selected():
    current_color = color_cb.get()
    current_style = ""

    if current_color == "red":
        current_style = "Sample.TLabel"
    elif current_color == "blue":
        current_style = "SampleTwo.TLabel"
    elif current_color == "yellow":
        current_style = "SampleThree.TLabel"
    elif current_color == "green":
        current_style = "SampleFour.TLabel"

    selection_lbl.configure(text="The selected color is {}".format(current_color), style=current_style)


# Callback associated to the OK button
def destroy_window():
    window.destroy()

window = tk.Tk()
window.title("Playground")
configure_style()

frm = ttk.Frame(window, style="Tab.TFrame")

# Create the selection label
selection_lbl = ttk.Label(frm, text="No option selected")
selection_lbl.pack()

# Creates the combo box with the options
colors = ["red", "green", "blue", "yellow"]
color_cb = ttk.Combobox(frm, values=colors)

# Associate a callback to the combo box.
# The callback is invoked when the selection
# in the combo box changes.
color_cb.bind("<<ComboboxSelected>>", \
        lambda event: combo_selected())
color_cb.pack()

# Create the destroy button
destroy_button = ttk.Button(frm, text="Destroy", command=destroy_window)
destroy_button.pack()

frm.pack()
window.mainloop()