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.
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 ofweight
(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
ofttk.Button
to assign the functionclick_ok_handler()
to the button. This means that whenever the user clicks the button OK, the functionclick_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()