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

The GUI playground is where you'll play with code to learn how to create a GUI with tkinter.

Open in Visual Studio Code the PistusResa skeleton that you modified in the last lab.

👉 Under folder gui, you'll find a file playground.py, where you can try the examples presented in this part.

Open a window

In order to open a window with title Playground, copy and paste the following code into the file ./gui/playground.py and execute it:

# 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")

The instruction window.mainloop() triggers the execution of the event loop.

During the event loop, the GUI listens for any event that might be triggered by the users' actions on the widgets, such as clicks on buttons, keypresses and so on. Importantly, calling the function window.mainloop() blocks any code that comes after it. If you look at the terminal in Visual Studio Code, you should read the message Opening the window, while the message The window is now closed does not appear. That message only appears when you close the window, which breaks the event loop.

Add a label to the window

The following code creates a window containing a label that bears the text "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()

You should observe that now the window is large enough to contain the label. This is the effect of using the function pack.

The function pack() invokes the pack geometry manager, that is responsible for placing the widgets in a window. The following GIF shows how the pack geometry manager works. First, the window is created with a certain size. Each time we invoke the function pack on a widget of that window, the geometry manager places the widget in the topmost portion of the window that is not occupied by another widget. When all the widgets are placed, the window is made large enough to hold them.


The pack geometry manager

We can use the argument side of the function pack to force the geometry manager to add the widgets to the left, right or bottom side of the window, instead of the top one.

An alternative geometry manager is grid that is used to place the widgets into a grid.


Replace the instruction:

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

with the following instruction:

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

Although the value of width and height is the same, the label isn't square. This is because the size is specified in text units.

👉 A text unit corresponds to the width (respectively, the height) of the character 0 (the number zero).

This choice guarantees that the appearance of the widget is consistent across platforms. The widget will always have the size to display 10 zeroes, both horizontally and vertically, independently of the font in use.

More widgets

Let's add some more widgets to our window: a text field and a button.

Copy the following code to the playground and read the comments.

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


Note that the function pack() is invoked for each widget.


Using ttk

Tkinter provides regular widgets (the ones that we used in the previous examples) that have a consistent look across all platforms; as a result, they look a bit primitive, and certainly different from the widgets that appear in other applications running on your operating system.

Since version 8.5, Tkinter made available a submodule called ttk containing themed widgets that are designed to look more native, that is similar to the widgets used in your operating system.


ttk widgets are more difficult to style (e.g., change the font, or the background color) than regular widgets. Styling configurations are already defined for you in file ./gui/gui_config.py.


In the following code we call function configure_style defined in file ./gui/gui_config.py. This function configures the look and feel of the different widgets used in the PistusResa GUI. We create the label, the text field and the button ttk.Label, ttk.Entry and ttk.Button respectively.

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

Using frames

In the previous examples, we added the widgets directly into the window. However, when the GUI consists of several widgets, it is useful to group them into frames; widgets that serve the same purpose (e.g. all the widgets used to collect a student's personal data) are placed in the same frame.

Since frames are independent of one another, each frame can use a different geometry manager.

In the following code, the label, text field and button are all placed in frame first_frame. Copy the code into the playground, read the comments and execute it. Note how the frame and the label are styled.

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

More on the pack geometry manager

If you try to resize the window, the frame (the yellow portion) sticks to the top side of the window. This is the default behaviour of the pack geometry manager. You can change it, by giving the parameter side of the function pack() one of the following values: tk.LEFT, tk.RIGHT and tk.BOTTOM.

👉 Try different values to see their effect.

You certainly noticed that the frame does not expand while resizing the window. We can use the parameters fill and expand to enable this behaviour.

The possible values of fill are:

  • tk.X. The frame will fill the parent window horizontally.
  • tx.Y. The frame will fill the parent window vertically.
  • tk.BOTH. The frame will fill the parent window both horizontally and vertically.

The possible values of expand are:

  • True. The frame will expand while the parent window is resized.
  • False. The frame does not expand.


Modify the code to make the label fill the parent frame (even when the frame is resized). Execute the code and try to resize the window.

Let's look at another example to better understand how the pack geometry manager works.

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


Explain the position of the widgets in the window.


The grid geometry manager

The pack geometry manager is quite powerful, but it's difficult to use. An equally powerful, yet more intuitive, geometry manager, is the grid geometry manager.

Try the following code.

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

This code creates four labels (with different background colors) and arrange them into a grid with two rows and two columns (note the use of the function grid). Rows and columns are identified by an index; both indices start at 0.

In the window created by the previous code, there is no space between the labels, which makes the GUI a little cluttered. We can free some space around the labels by using the arguments padx and pady of the function grid, as follows:

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)

Using two frames

In the following example, we add two frames to the window; each frame uses the grid geometry manager, but in the top frame (the yellow one) the labels are arranged into a 4x4 grid, while in the bottom frame (the blue one) the labels are arranged into a 1x3 grid.

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

In the window produced by the previous code, the top frame doesn't fill the window. Modify the code to do so. Execute the program.


columnconfigure and rowconfigure

In the window produced by the code that you modified, the labels in the top frame stick to the left side of the frame.

If we want them to be arranged uniformly across the frame, we need to call the function columnconfigure() on the top frame, as follows (put these two lines right before the instruction that you modified in the previous question):

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

👉 The function columnconfigure() takes in two arguments:

  • the index of the column to configure;
  • the weight that sets the rate of expansion of the widgets (typically, one uses 1) when the frame is resized. You can try different values of weight (not necessarily the same value for the two columns) and resize the window to see the effect.

An equivalent function rowconfigure() exists.

Alignment of a widget

We note that all labels are placed right at the center of their respective cells. We can change this behaviour by using the argument sticky of the function grid(). The possible values of sticky are:

  • 'w'. West (or, left) alignment;
  • 'e'. East (or, right) alignment;
  • 'n'. North (or, top) alignment;
  • 's'. South (or, bottom) alignment;
  • any concatenation of these values (e.g., 'ew').

For instance, change the placement of the label at (0, 0) in the top frame with the following code:

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

Since we use the value 'ew' for the argument sticky, the label spans horizontally the whole cell. You can play with different values to see what you obtain.

Event handling

The interfaces that we created so far do not react to user's actions. Each action (e.g., typing text in a text field, clicking on a button) generates an event. For each event, we have to write the code to be executed in response to that event; in other words, we associate to each event a function (event handler or callback) that is called when the event happens.

Try the following code.

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

This code:

  • defines a function click_ok_handler() that prints the message "The user clicked OK" to the terminal.
  • creates a frame and adds a button.
  • uses the parameter command of ttk.Button to assign the function click_ok_handler() to the button. This means that whenever the user clicks the button OK, the function click_ok_handler() is invoked.

Execute the code and try for yourself. Each time you click the button OK, the message "The user clicked OK" should appear in the Visual Studio Code terminal.

A more complete example

The interface created by the following code consists of a text field and a button. Initially, the button is disabled. When you type something in the text field, its content is printed to the Visual Studio Code terminal; the button is enabled only if the last character in the text field is "#".

👉 Copy this code and paste it into Visual Studio Code. Read the comments to understand the 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()


Execute the code and try the interface.

  • Write something in the text field and then remove it. Look at the output in the Visual Studio Code terminal. What happens?
  • Modify the code to fix the problem.


Radio buttons

A radio button is a widget that lets users choose one of many mutually exclusive options.

The following code creates two radio buttons, "Enable" and "Disable"; when the first is selected, the OK button is enabled; when the second is selected, the OK button is disabled.

👉 At most one radio button can be selected at any time.

👉 Copy the following code into Visual Studio Code, read the comments to understand how to use radio buttons and execute it.

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

Combo boxes

A combo box is a widget that lets users select an option out of a set of possibilities.

The following code creates an interface with three widgets:

  • A combo box with four choices: "red", "green", "blue", "yellow".
  • A label, where a text is shown describing the currently selected option.
  • A button used to close the window.

👉 Copy the code into Visual Studio Code, read the comments and execute it.


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