CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Systèmes d'information et programmation - TD: Interfaces graphiques avec Python

Activités de ce TD :

  • comprendre les principaux éléments d'une interface graphique.
  • apprendre à utiliser tkinter pour créer des interfaces graphiques en Python.
  • enrichir l'interface graphique de PistusResa avec une fenêtre de connexion.

Introduction

Une interface graphique (GUI) permet à tout utilisateur de PistusResa d'accéder à toutes les fonctionnalités de l'application de manière simple et intuitive.


Plusieurs bibliothèques, connues sous le nom de boîte à outils de composants d'interface graphique (en anglais, widget toolkits), existent pour programmer une interface graphique en Python. Pour ce tutoriel, nous avons choisi Tkinter, qui est construit sur la bibliothèque Tcl/Tk, pour deux raisons :

  • Il s'agit de la bibliothèque standard d'interface graphique intégrée à Python.
  • Contrairement à d'autres bibliothèques, elle est facile à apprendre.

Par contre, les interfaces programmées avec Tkinter ont l'air un peu rudimentaires. Cependant, si nous avons besoin de développer rapidement des interfaces simples, Tkinter est un excellent choix.


La figure suivante montre la fenêtre de connexion de PistusResa. Les principaux éléments d'une interface graphique sont les suivants :

  • Fenêtre, c'est-à-dire un conteneur qui inclut tous les autres éléments de l'interface graphique, généralement appelés widgets (en français, composants d'interface graphique).
  • Barre de titre : où apparaît le titre de la fenêtre.
  • Champs de texte : où les utilisateurs peuvent saisir du texte.
  • Étiquettes : utilisées pour expliquer la fonction des autres widgets.
  • Boutons : utilisés par les utilisateurs pour déclencher une réaction.

Ces widgets ne sont généralement pas ajoutés directement à la fenêtre ; ils sont plutôt regroupés dans des "frames". Dans la figure, il y a trois frames (invisibles dans la figure) :

  • le premier contient les étiquettes et les champs de texte Username et Password ;
  • le deuxième contient l'étiquette du message (Enter the username (at least 5 characters)) ;
  • le troisième contient les boutons.

L'avantage d'utiliser des frames est que l'on peut disposer les widgets selon différents critères, comme nous le verrons plus loin.


The PistusResa login window


Une grosse partie de l'interface graphique de PistusResa est déjà implémentée. Votre objectif aujourd'hui est d'ajouter une fenêtre de connexion, comme celle montrée dans la figure.


Se familiariser avec l'interface graphique

Dans la première partie de ce TD, vous apprendrez à utiliser tkinter.

⏰ Temps à consacrer à cette partie : au maximum 1 heure.

👉 Continuez à lire sur cette page.

La fenêtre de connexion

Vous êtes maintenant prêt à créer la fenêtre de connexion.


Il est important de comprendre comment l'application PistusResa est exécutée en premier lieu.

Le point d'entrée de l'application est le fichier pistus.py.


Essayez d'exécuter le fichier pistus.py dans Visual Studio Code. Si vous obtenez des erreurs lorsque vous exécutez le fichier à l'aide du bouton d'exécution de Visual Studio Code, vous devez ouvrir un terminal et taper : py -m pistus (Windows) ou python3 -m pistus (macOS). Cela garantira que toutes les importations dans tous les fichiers fonctionneront.

👉👉ATTENTION : Si vous obtenez une erreur AttributeError: module 'PIL.Image' has no attribute 'ANTIALIAS, ouvrez le fichier gui_config.py et changez Image.ANTIALIAS avec Image.LANCZOS à la line 18.👈👈


Lorsque vous exécutez pistus.py, les instructions suivantes sont exécutées (lisez le code pour identifier ces actions) :

  1. Charger la configuration de l'application du fichier ./config/config.
  2. Charger le paquet de messages dans la langue spécifiée dans la configuration. Le paquet de messages contient le texte associé aux widgets.
  3. Se connecter à la base de données, dont le chemin d'accès est spécifié dans le fichier de configuration.
  4. Si l'autorisation est activée dans le fichier de configuration, PistusResa appelle la fonction open_login_window() définie dans le fichier ./gui/login.py.
  5. Si l'autorisation n'est pas activée, la fenêtre principale de l'application s'affiche.
  6. Chaque fois que l'utilisateur ferme la fenêtre principale de l'application, PistusResa ferme la connexion à la base de données et arrête son exécution.

👉 Si vous regardez le fichier de configuration, l'autorisation est activée. Cependant, si vous exécutez le fichier pistus.py maintenant, aucune fenêtre n'apparaîtra car la fenêtre de connexion n'est pas encore implémentée.

L'interface graphique de la fenêtre de connexion

Vous allez maintenant créer les widgets de la fenêtre de connexion.

Ouvrez le fichier ./gui/login.py et allez à la fonction open_login_window().

Cette fonction est appelée lorsque PistusResa est exécuté et que l'authentification est activée. La fonction crée l'interface graphique de la fenêtre. En particulier, elle appelle les trois fonctions suivantes :

  • _credentials_frm_widgets(). Crée le frame contenant les champs de texte pour saisir le nom d'utilisateur et le mot de passe.
  • _message_frm_widgets(). Crée le frame contenant une étiquette utilisée pour afficher des messages à l'utilisateur (par exemple, "mot de passe incorrect").
  • _buttons_frm_widgets(). Crée le frame contenant les boutons.

👉 Votre tâche consiste à mettre en œuvre ces trois fonctions. Pour chaque fonction :

  • Lisez les commentaires dans le code pour comprendre ce qu'il faut faire.
  • Les variables pour les champs de texte, les étiquettes et les boutons sont déjà définies au début de la fonction. Utilisez ces variables et ne changez pas leur nom.
  • Ecrivez le code à l'endroit indiqué (après la phrase "TODO").


Complétez l'implémentation de la fonction _credentials_frm_widgets().

👉 À la fin de la fonction, les champs de texte que vous créez sont ajoutés à une variable globale text_fields, un dictionnaire. Ne l'oubliez pas, car vous devrez utiliser cette variable globale plus tard lorsque vous ajouterez des fonctions de rappel (en anglais, callbacks) à vos widgets.



ANSWER ELEMENTS

    ttk.Label(credentials_frm, text = username_lbl_text)\
        .grid(row=0, column=0, padx=10, pady=10, sticky = "w")

    # Adds the text field "username"
    username_tf_content = tk.StringVar(value="")
    username_tf = ttk.Entry(credentials_frm, textvariable=username_tf_content)
    username_tf.grid(row = 0, column = 1, sticky='ew')

    # Adds the label "password".
    ttk.Label(credentials_frm, text = password_lbl_text)\
        .grid(row = 1, padx=10,pady=10, column = 0, sticky = "W")

    # Adds the text field "password".
    password_tf_content = tk.StringVar(value="")
    password_tf = ttk.Entry(credentials_frm, show='*', \
                            textvariable=password_tf_content)
    password_tf.grid(row = 1, column = 1, sticky='ew')

    # The text fields will span the entire column.
    credentials_frm.columnconfigure(1, weight=1)    


Complétez l'implémentation de la fonction _message_frm_widgets().

👉 À la fin de la fonction, les étiquettes que vous avez créées sont ajoutées à une variable globale control_labels, un dictionnaire. Ne l'oubliez pas, car vous devrez utiliser cette variable globale plus tard lorsque vous ajouterez des fonctions de rappel à vos widgets.



ANSWER ELEMENTS

    message_lbl = ttk.Label(message_frm, style="Check.TLabel")
    message_lbl.grid(row=0, column=0, sticky='ew')


Complétez l'implémentation de la fonction _buttons_frm_widgets().

👉 À la fin de la fonction, les boutons que vous créez sont ajoutés à une variable globale buttons, un dictionnaire. Ne l'oubliez pas, car vous devrez utiliser cette variable globale plus tard lorsque vous ajouterez des fonctions de rappel à vos widgets.


Gestion des événements

L'automate suivant montre comment la fenêtre de connexion change en fonction des actions de l'utilisateur sur ses widgets.




Avez-vous besoin d'une description textuelle de l'automate ? Cliquez ici
  1. Initialement, la fenêtre est dans l'état INIT. Le champ de texte password et le bouton login sont désactivés ; l'étiquette de contrôle affiche le message messages_bundle["enter_username"] ;
  2. Pendant que l'utilisateur tape son nom d'utilisateur, celui-ci est vérifié pour s'assurer qu'il répond à certains critères de validité (username_ok) ; si c'est le cas (le mot de passe n'est pas OK parce que l'utilisateur n'a encore rien tapé), la fenêtre passe à l'état USERNAME_ENTERED ;
  3. Dans l'état USERNAME_ENTERED, seul le bouton login est désactivé ; l'étiquette de contrôle affiche le message messages_bundle["enter_password"] ;
  4. Si l'utilisateur modifie le nom d'utilisateur et que celui-ci ne satisfait pas à la contrainte de validité, la fenêtre repasse à l'état INIT ;
  5. Si l'utilisateur tape un mot de passe qui répond à certains critères de validité, la fenêtre passe à l'état CREDENTIALS_ENTERED ; le message messages_bundle["login_authorized"] est affiché dans l'étiquette de contrôle ;
  6. Dans l'état CREDENTIALS_ENTERED, tous les champs sont activés, y compris le bouton login ;
  7. Si l'utilisateur modifie le mot de passe et que le mot de passe ne répond pas aux critères de validité, la fenêtre revient à l'état USERNAME_ENTERED ;
  8. Si l'utilisateur modifie le nom d'utilisateur et que celui-ci ne satisfait pas aux critères de validité, la fenêtre revient à l'état INIT ; le champ de texte du mot de passe est désactivé, mais sa valeur actuelle est conservée ;
  9. Si l'utilisateur modifie le nom d'utilisateur et que le nom d'utilisateur et le mot de passe répondent tous deux aux critères de validité, la fenêtre passe directement à l'état CREDENTIALS_ENTERED ;
  10. Si l'utilisateur clique sur le bouton login, et que le nom d'utilisateur et le mot de passe ne sont pas corrects, la fenêtre reste dans l'état CREDENTIALS_ENTERED et le message messages_bundle["username_not_found"] ou messages_bundle["incorrect_password"] est affiché dans l'étiquette de contrôle ;
  11. Si l'utilisateur clique sur le bouton login, et que le nom d'utilisateur et le mot de passe sont corrects, la fenêtre de connexion est détruite et la fenêtre principale de PistusResa est ouverte ;

Quel que soit l'état, si l'utilisateur clique sur le bouton clear, tous les champs sont effacés et la fenêtre revient à l'état INIT ; si l'utilisateur clique sur le bouton cancel, la fenêtre de connexion est détruite.


L'animation suivante peut vous aider à comprendre le comportement de la fenêtre de connexion en fonction des actions de l'utilisateur.



Implémentation de l'automate

Vous allez maintenant implémenter les fonctions nécessaires pour réaliser le comportement décrit dans l'automate discuté ci-dessus.

👉 Vous trouverez les fonctions à implémenter dans le fichier login.py.


Suivez les instructions dans les commentaires pour implémenter les fonctions suivantes :

  • init_state().
  • username_entered_state().
  • credentials_entered_state().
  • username_updated().
  • password_updated().

👉 Rappelez-vous que les champs de texte, les étiquettes de contrôle et les boutons sont stockés dans les variables globales text_fields, control_labels et buttons respectivement.

👉 N'oubliez pas d'ajouter les instructions nécessaires à la fonction _credentials_frm_widgets() pour associer les fonctions username_updated et password_updated en tant que fonctions de rappel des champs de texte username et password.

👉 Exécutez pistus.py et vérifiez que la fenêtre change correctement d'état lorsque vous tapez le nom d'utilisateur et le mot de passe.

Warning. Même si le nom d'utilisateur et le mot de passe correspondent à tous les critères de validité, vous ne pouvez pas vous connecter à l'application. Vous devrez attendre la section suivante.



ANSWER ELEMENTS

Fonction init_state:

    text_fields["username"][0].configure(state=["!disabled"])
    text_fields["password"][0].configure(state=["disabled"])
    buttons["login"].configure(state=["disabled"])
    control_labels["message_ctrl"].\
        configure(text=messages_bundle["enter_username"])

Fonction username_entered_state:

    text_fields["username"][0].configure(state=["!disabled"])
    text_fields["password"][0].configure(state=["!disabled"])
    buttons["login"].configure(state=["disabled"])
    control_labels["message_ctrl"].\
        configure(text=messages_bundle["enter_password"])

Fonction credentials_entered_state:

    text_fields["username"][0].configure(state=["!disabled"])
    text_fields["password"][0].configure(state=["!disabled"])
    buttons["login"].configure(state=["!disabled"])
    control_labels["message_ctrl"].configure(text=message)

Fonction username_updated:

    if utils.username_ok(get_username()):
        if not utils.password_ok(get_password()):
            username_entered_state()
        else:
            credentials_entered_state(messages_bundle["login_authorized"])
    else:
        init_state()

Fonction password_updated:

    if utils.password_ok(get_password()):
        if not utils.username_ok(get_username()):
            init_state()
        else:
            credentials_entered_state(messages_bundle["login_authorized"])
    else:
        if not utils.username_ok(get_username()):
            init_state()
        else:
            username_entered_state()

Dans la fonction _credentials_frm_widgets, nous devons ajouter les deux instructions suivantes afin d'associer les deux fonctions de rappel username_updated et password_updated aux champs de texte username et password.

username_tf_content.trace("w", username_updated)
password_tf_content.trace("w", password_updated)

Connexion

Vous allez maintenant mettre en œuvre trois fonctions qui vous permettront de vous connecter à PistusResa.

👉 Le nom d'utilisateur est admin et le mot de passe est Adm1n!


  • Lisez les commentaires dans le fichier login.py pour implémenter les fonctions login(), clear() et cancel().
  • Ajoutez les instructions nécessaires à la fonction _buttons_frm_widgets pour associer login(), clear() et cancel() comme fonctions de rappel des boutons login, clear et cancel respectivement.
  • Exécutez pistus.py pour vérifier que vous pouvez vous connecter et ouvrir la fenêtre principale de PistusResa.


ANSWER ELEMENTS

Fonction login :

    if res[0]:
        window.destroy()
        open_main_window(cursor, conn, messages_bundle, lang)
    elif res[1] == auth.INCORRECT_PASSWORD:
        credentials_entered_state(messages_bundle["incorrect_password"])
    elif res[1] == auth.USERNAME_NOT_FOUND:
        credentials_entered_state(messages_bundle["username_not_found"])

Function clear :

    text_fields["username"][1].set("")
    text_fields["password"][1].set("")

Fonction cancel :

    clear()
    window.destroy()

Dans la fonction _buttons_frm_widgets, nous devons ajouter les arguments command=login, command=clear et command=cancel lorsque nous créons les boutons login, clear et cancel respectivement.



La fenêtre principale

Si vous vous connectez à PistusResa, vous devriez voir la fenêtre principale de l'application. Cette fenêtre comporte un menu sur la gauche avec trois options qui permettent aux utilisateurs d'ouvrir des fenêtres pour ajouter et modifier les données et les inscriptions des élèves.

Jouez avec l'interface et vérifiez que les fonctions que vous avez codées dans le module Student fonctionnent correctement.

Que faire ensuite ? (Facultatif)

Si vous avez encore du temps, vous pouvez compléter l'application et implémenter le module d'authentification et le module deadline.