CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Information Systems and Programming - Lab: Graphical interfaces with Python

Activities of this lab:

  • understand the main items of a graphical interface.
  • learn how to use tkinter to create graphical interfaces in Python.
  • enrich the graphical interface of PistusResa with a login window.

Introduction

The Graphical User Interface (GUI) allows any PistusResa user to access all the application functionalities in an easy and intuitive way.


Several libraries, known as widget toolkits, exist to program a GUI in Python. For this tutorial, we choose Tkinter, which is built on top of the Tcl/Tk widget toolkit, for two reasons:

  • It is the standard built-in Python GUI library.
  • As opposed to other libraries, it is easy to learn.

As a downside, the interfaces programmed with Tkinter look a bit rudimentary. However, if we need to quickly develop simple interfaces, Tkinter is an excellent choice.


The following figure shows the PistusResa login window. The main elements of a GUI are:

  • Window, that is a container that includes all the other GUI elements, usually referred to as widgets.
  • Title bar: where the title of the window appears.
  • Text fields: where users can enter some text.
  • Labels: used to explain the function of other widgets.
  • Buttons: used by users to trigger a reaction.

These widgets are usually not added to the window directly; instead, they are grouped into frames. In the figure there are three frames (invisible in the figure):

  • the first frame includes the Username and Password labels and text fields;
  • the second includes the message label (Enter the username (at least 5 characters));
  • the third includes the buttons.

The advantage of using frames is that we can arrange widgets with different criteria, as we'll see below.


The PistusResa login window


Most of the PistusResa GUI is already implemented. Your objective today is to add a login window, as the one shown in the figure.


The GUI playground

In the first part of this lab, you'll learn how to use tkinter.

⏰ Time to spend in this part: at most 1 hour.

👉 Continue reading on this page.

The login window

You're now ready to create the login window.


It is important to understand how the application PistusResa is executed in the first place.

The entry point of the application is file pistus.py.


Try to run file pistus.py in Visual Studio Code. If you get errors when you execute the file through the Visual Studio Code execution button, you should open a terminal and type: py -m pistus (Windows) or python3 -m pistus (macOS). This will ensure that all the imports in all files will work.


When you execute pistus.py, the following instructions are executed (read the code to identify these actions):

  1. Load the application configuration from file ./config/config.
  2. Load the messages bundle in the language specified in the configuration. The messages bundle contains the text associated to the widgets.
  3. Connect to the database, of which the file path is specified in the configuration file.
  4. If authorization is enabled in the configuration file, PistusResa calls the function open_login_window() defined in file ./gui/login.py.
  5. If authorization is not enabled, the main window of the application pops up.
  6. Whenever the user closes the application main window, PistusResa closes the connection to the database and stops its execution.

👉 If you look at the configuration file, authorization is enabled. However, if you run the file pistus.py right now, no window will appear because the login window is not implemented yet.

The login window GUI

You'll now create the widgets of the login window.

Open file ./gui/login.py and go to function open_login_window().

This function is called when PistusResa is executed and authentication is enabled. The function creates the GUI of the window. In particular, it calls the three following functions:

  • _credentials_frm_widgets(). Creates the frame containing the text fields to enter username and password.
  • _message_frm_widgets(). Creates the frame containing a label used to show messages to the user (e.g., "incorrect password").
  • _buttons_frm_widgets(). Creates the frame containing the buttons.

👉 Your job is to implement these three functions. For each function:

  • Read the comments in the code to understand what to do.
  • The variables for the text fields, labels and buttons are already defined at the beginning of the function. Use these variables and don't change their name.
  • Write the code where indicated (after the phrase "TODO").


Complete the implementation of function _credentials_frm_widgets().

👉 At the end of the function, the text fields that you create are added to a global variable text_fields, a dictionary. Remember this, as you'll need to use this global variable later when adding callbacks to your 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)    


Complete the implementation of function _message_frm_widgets().

👉 At the end of the function, the labels that you create are added to a global variable control_labels, a dictionary. Remember this, as you'll need to use this global variable later when adding callbacks to your widgets.



ANSWER ELEMENTS

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


Complete the implementation of function _buttons_frm_widgets().

👉 At the end of the function, the buttons that you create are added to a global variable buttons, a dictionary. Remember this, as you'll need to use this global variable later when adding callbacks to your widgets.


Event handling

The following automaton shows how the login window changes as a result of the user's actions on its widgets.




Do you need a textual description of the automaton? Click here
  1. Initially, the window is in the INIT state. The password text field and the button login are disabled; the control label shows the message messages_bundle["enter_username"];
  2. While the user types the username, the username is checked to verify that it meets certain validity criteria (username_ok); if so (the password is not OK because the user hasn't typed anything yet), the window transitions to the USERNAME_ENTERED state;
  3. In the USERNAME_ENTERED state, only the button login is disabled; the control label shows the message messages_bundle["enter_password"];
  4. If the user changes the username, and the username does not meet the validity constraint, the window transitions back to the INIT state;
  5. If the user types a password that meets certain validity criteria, the window transitions to the state CREDENTIALS_ENTERED; the message messages_bundle["login_authorized"] is shown in the control label;
  6. In the CREDENTIALS_ENTERED state, all fields are enabled, including the button login;
  7. If the user changes the password, and the password does not meet the validity criteria, the window transitions back to the state USERNAME_ENTERED;
  8. If the user changes the username, and the username does not meet the validity criteria, the window transitions back to the state INIT; the password text field will be disabled, but its current value is maintained;
  9. If the user changes the username, and both the username and the password meet the validity constraints, the window transitions directly to the state CREDENTIALS_ENTERED;
  10. If the user clicks the button login, and the username and password are not correct, the window stays in the state CREDENTIALS_ENTERED and the message messages_bundle["username_not_found"] or messages_bundle["incorrect_password"] is displayed in the control label;
  11. If the user clicks the button login, and the username and password are correct, the login window is destroyed and the PistusResa main window is opened;

In any state, if the user clicks the button clear, all fields are cleared and the window transitions back to the state INIT; if the user clicks the button cancel, the login window is destroyed.


The following animation might help you understand the behaviour of the login window based on the user's actions.



Automaton implementation

You'll now implement the functions necessary to realize the behaviour described in the automaton discussed above.

👉 You'll find the functions to implement in file login.py.


Follow the instructions in the comments to implement the following functions:

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

👉 Remember that the text fields, control label and the buttons are stored in the global variables text_fields, control_labels and buttons respectively.

👉 Don't forget to add the necessary instructions to function _credentials_frm_widgets() to associate the functions username_updated and password_updated as callbacks of the text fields username and password.

👉 Run pistus.py and verify that the window correctly changes state when you type the username and the password.

Warning. Even if username and password match all the validity criteria, you cannot log in to the application. You'll have to wait the next section.



ANSWER ELEMENTS

Function 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"])

Function 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"])

Function 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)

Function 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()

Function 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()

In function _credentials_frm_widgets we need to add the two following instructions in order to associated the two callbacks username_updated and password_updated to the text fields username and password.

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

Logging in

You'll now implement three functions that will let you finally log in to PistusResa.

👉 The username is admin and the password is Adm1n!


  • Read the comments in file login.py to implement the functions login(), clear() and cancel().
  • Add the necessary instructions to function _buttons_frm_widgets to associate login(), clear() and cancel() as callbacks of the buttons login, clear and cancel respectively.
  • Run pistus.py to verify that you can successfully log in and open the PistusResa main window.


ANSWER ELEMENTS

Function 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("")

Function cancel:

    clear()
    window.destroy()

In function _buttons_frm_widgets we need to add the argument command=login, command=clear and command=cancel when we create the login, clear and cancel buttons respectively.



The main window

If you could log in to PistusResa, you should see the application main window. This window has a menu on the left with three options that let the users open windows to add and edit student data and registrations.

👉 Play with the interface and verify that the functions that you coded in the Student module work correctly.

What's next? (Optional)

If you still have time, you can complete the application and implement the authentication and deadline module.