Graphical User Interfaces
We are finally up to the part where we will create an interface for our program. Designing program interfaces is a massive subject, and designing useful and great looking interfaces is another subject in itself. We are only going to look at the basic tools of graphical user interfaces (which I will now refer to as GUIs, pronounced gooey) and we won't be worrying about making good looking interfaces.
There are many different tools you can use to create GUIs, but we will be using the one included in Python 3 called 'tkinter'. It's definitely not the greatest one out there, but it does the job nicely for our programs. The language style doesn't match perfectly to Python so you may notice some small differences.
Event Driven Programming
When writing a program to be used through a GUI we need to change from sequential programming to event driven programming. A program using sequential programming will always do things in the same order each time it is run. These are the programs we started off writing when learning Python, like a shopping calculator for example. But event driven programs, the user controls the behaviour of the program. When we create a program with a GUI, our code will:
- Setup the program up and display the GUI.
- Wait for an action from the user.
- Do something for that action.
- Go back to step 2.
When using a GUI system, there are three major tools they give us:
- Interface components/Widget set: These are the things you see on an interfaces like windows, buttons, text boxes, scroll bars, radio buttons. There are loads of different components, but we will just be looking at a small selection of these.
- Geometry management: This is the system for putting the components above together. There are two geometry managers we can use: pack or grid. We will be using one that use a grid layout as it's the easiest to understand.
- Event binding: This gives us the ability to trigger some code from an action on a component.
Let's look at creating a simple interface to start off, and see how these tools tie together:
Our First Interface: Hello World Visualised
Let's create an interface which has the words "Hello World!" displayed. Have a look at the code below, type it out and run it for yourself, and see if you can decipher it. We are going to write small snippets of test code, but later will wrap all this code in a GUI class (but more on that later!).
from tkinter import * from tkinter.ttk import * window = Tk() hello_label = Label(window, text='Hello World!') hello_label.grid(row=0, column=0) window.mainloop()
The first two lines of our program import the GUI toolkit from the Python libraries. The terminology stands for 'from the tkinter python library, import everything in the library' as the star symbol stands for everything/wildcard. The second line imports some extra features for tkinter that we will use.
window = Tk() creates a window by calling the main tkinter function
Tk(). We assign this to a variable, so when we create other components we can tell them to be displayed in this window. In the interfaces we will be creating, we will only be using single window interfaces, so we will be placing all components into this window.
The sixth line of the program is
hello_label = Label(window, text='Hello World!') and there is quite a bit happening here, so let's break it down. We are creating an Label component which is used to display text and it is taking two parameters. The first is the window the component should be placed into (but it isn't placed into it just yet), in this case we use the window variable we created above. The next parameter is setting the text to "Hello World!", without this parameter the label would be blank! We finally save this Label in the variable
The seventh line of the program places the component we created with the grid geometry manager. We run the grid function on the variable containing the label, which takes two number parameters, the row and column position. It works like map co-ordinates, but it starts counting from 0. Check the table below for an explaination.
|row 0 col 0||row 0 col 1||row 0 col 2|
|row 1 col 0||row 1 col 1||row 1 col 2|
|row 2 col 0||row 2 col 1||row 2 col 2|
The last line in the main function
window.mainloop() triggers the function on the window to start waiting for actions from the user. The program will stop at this line of code until the window is closed, so this will be the last line our the programs.
Let's take a look at the several components tkinter offers that we will be using in our programs.
When writing a program using tkinter, you can ask the interface to update a component when a certain variable is changed. There’s no way to track changes to Python variables, but tkinter allows you to create special variable types that can be used wherever tkinter requires a variable.
We will need to use these variables as there will be components of the interface the will need to change. If you remember our Label above, it was set to one value on creation. Instead of connecting a component to a value, we will start connecting these to variables which we can update. If a Label doesn't need to change (like a heading) then we could just use the text property.
StringVar for strings and
IntVar for integers (there is also
DoubleVar if you need them). We can set the value of these variables by running a
.set(value) function on them with a value. We can also run
.get() to get the current value. Let's see this in action by setting a label to a variable:
from tkinter import * from tkinter.ttk import * window = Tk() data = StringVar() data.set('Hello World!') hello_label = Label(window, textvariable=data) hello_label.grid(row=0, column=0) window.mainloop()
At the moment this seems fairly pointless, but as we introduce input methods like buttons and text entries then these variables are extremely useful.
We have already been using the label component and have seen the two parameters we will be using:
- text - Displays one or more lines of text. To use a line break, use a newline character (
- textvariable - Sets a variable to display text on this label.
There are loads of extra parameters that allow you to customise height, width, fonts, and colours but these aren't necessary for the assessment. You can read more about these parameters here.
Buttons are extremely useful and we will using plenty of these in our interfaces. We can create a button with the code
hello_button = Button(window, text="Say hello", command=sayHello). It's very similar to creating a Label with a new parameter. The new parameter is called command, and determines what function to run when the button is clicked, in this case it's
The last parameter in creating a button is
command=sayHello. This the name of the function to run when the button is pressed. Note it's not a function call (there are no brackets after it).
With all that knowledge, let's try out a small example:
from tkinter import * from tkinter.ttk import * def sayHello(): data.set("Hello World!") window = Tk() data = StringVar() hello_label = Label(window, textvariable=data) hello_label.grid(row=0, column=0) hello_button = Button(window, text="Say hello", command=sayHello) hello_button.grid(row=1, column=0) window.mainloop()
In this case the program starts with a blank label, but if the user clicks the button it will set the label text.
Exercise: Graphical Greetings
Using the program above as a starter, create a program that has two buttons and a label. One button should change the text label to "Hello!", while the other button changes the text to "Bye!".
Exercise: Two Function Increments
Create a program that uses a label and two buttons, that keeps track of a number counter, using an IntVar variable type. One button should add one onto the value, while the other button should subtract one from the value. When designing the functions for each button, we need to get the current value, change the value depending on the button press, then set the new value.
from tkinter import * from tkinter.ttk import * def incrementValue(): # Your code here def decrementValue(): # Your code here window = Tk() value = IntVar() value.set(0) # Setting the starting value is good practice value_label = Label(window, textvariable=value) value_label.grid(row=0, column=0, columnspan=2) value_up_button = Button(window, text="+1", command=incrementValue) value_up_button.grid(row=1, column=0) value_down_button = Button(window, text="-1", command=decrementValue) value_down_button.grid(row=1, column=1) window.mainloop()
Using buttons that trigger functions with parameters
The command property of a button can only take a name of a function. A common mistake is entering the function by
command=sayHello() which calls the function before the component is created, gets the return value and converts this to a string. Then on a button click, tkinter searches for a function with the name of the string, definitely not what we want.
What we can use are helper functions that call our function with parameters. For example if we have a greeting program that had multiple buttons that greeted differently, we could have a function for each that changes the label. But to reduce repetition in our code, we can have a function called
change_message(greeting) that takes a greeting to display. Check out the code below for an example:
from tkinter import * from tkinter.ttk import * def changeMessage(greeting): message_var.set(greeting) def ahoy(): changeMessage("Ahoy!") def avast(): changeMessage("Avast ye!") def aye(): changeMessage("Aye aye!") window = Tk() message_var = StringVar() message_label = Label(window, textvariable=message_var) message_label.grid(row=0, column=0, columnspan=3) ahoy_button = Button(window, text="Greeting 1", command=ahoy) ahoy_button.grid(row=1, column=0) avast_button = Button(window, text="Greeting 2", command=avast) avast_button.grid(row=1, column=1) aye_button = Button(window, text="Greeting 3", command=aye) aye_button.grid(row=1, column=2) window.mainloop()
For those who are interested, you can simplify the call to a helper function by creating it inline with a lambda expression like
Exercise: Helper Functions
Complete the program below by entering functions so the program can modify the value either by adding 1, adding 5, subtracting 1, or subtracting 5. You are only allowed to use one
.set() and one
from tkinter import * from tkinter.ttk import * # Write your functions here window = Tk() value_var = IntVar() value_label = Label(window, textvariable=value_var) value_label.grid(row=0, column=0, columnspan=4) subtract_five_button = Button(window, text="- 5", command=subtractFive) subtract_five_button.grid(row=1, column=0) subtract_one_button = Button(window, text="- 1", command=subtractOne) subtract_one_button.grid(row=1, column=1) add_one_button = Button(window, text="+ 1", command=addOne) add_one_button.grid(row=1, column=2) add_five_button = Button(window, text="+ 5", command=addFive) add_five_button.grid(row=1, column=3) window.mainloop()
Getting input from the user is crucial to many programs, and a text entry box is a great way to get a wide range of inputs through an interface. It connects to a tkinter variable like a Label.
age_var = IntVar() age_entry = Entry(window, textvariable=age_var) age_entry.grid(row=2, column=0)
Users can enter any symbols (text, numbers, whitespace, etc) into this box. When running a function from a button press, we can get the text in the entry by calling
.get() on the variable. If the values in the entry are not valid with the variable type (for example, letters in a IntVar) then the program will through an exception when calling
.get(). This is important to remember when choosing variable types, and we will look at how to hand these exceptions later.
Exercise: Saying Hello
Create a program that resembles the image below. The interface uses one text entry for a name, one button, and two labels. The button should have the text 'Say hello' and when the user clicks the button, the bottom label should display the name with 'Hi' in front of it.
Create a program that uses a text entry for the user to enter a number and has a button called "Double it!" that, when clicked, takes the number currently in the text field, doubles it, and displays the result using a label. It should look like the image below:
from tkinter import *
from tkinter.ttk import *
value_str = str(value.get())
double_str = str(value.get() * 2)
result.set(value_str + " times 2 is " + double_str)
window = Tk()
value = IntVar()
value.set(0) # Setting the starting value is good practice
value_entry = Entry(window, textvariable=value)
double_button = Button(window, text="x 2", command=doubleValue)
result = StringVar()
result_label = Label(window, textvariable=result)
We can use an entry to get input from the user, but sometimes it's nicer to restrict the input choices. A combo box (commonly called a dropdown) can give the user the choice of picking an item from a list of provided choices. Let's look at an example combo box and break it down:
fruit_choices = ["Apple", "Banana", "Cherry", "Dragon Fruit"] selected_fruit = StringVar() selected_fruit.set(fruit_choices) fruit_combo = Combobox(window, textvariable=selected_fruit, values=fruit_choices) fruit_combo.grid(row=0, column=0)
When creating a combo box there are two properties we need to use. The first is a textvariable as we have used before, and it is set to the currently selected item. The other property is
values, which is the options the combo box should display. This needs to be given as a list of strings, so the first line of the code above creates a list of options. When a combo box is created it doesn't start with anything selected, so the third line above sets the variable to the first option of our choices.
Create a basic calculator program that uses two entries, a combo box, a label, and a button to do basic calculations. The combo box should have the options for addition, subtraction, and multiplication. See the image below for how the calculator could look.
from tkinter import *
from tkinter.ttk import *
selected = operation_var.get()
if selected == "+":
result = value_1_var.get() + value_2_var.get()
elif selected == "-":
result = value_1_var.get() - value_2_var.get()
result = value_1_var.get() * value_2_var.get()
window = Tk()
value_1_var = IntVar()
left_entry = Entry(window, textvariable=value_1_var)
operations = ["+","-","x"]
operation_var = StringVar()
operations_combobox = Combobox(window, textvariable=operation_var, values=operations)
value_2_var = IntVar()
right_entry = Entry(window, textvariable=value_2_var)
result_var = StringVar()
result_label = Label(window, textvariable=result_var)
result_label.grid(row=1, column=0, columnspan=3)
calculate_button = Button(window, text="Calculate", command=calculate)
calculate_button.grid(row=2, column=0, columnspan=3)
There are many more components including check buttons, scrollbars, menus, frames, and canvases however the components we looked at above will be fine for what we wish to achieve.
Possibly could talk about rowspan, columnspan, and formatting of labels (with font etc).
Exceptions on input?
Objects and GUIs
Nearly all the example programs so far have used code in the global scope. But we want to avoid globals for a well designed program!
The solution is to wrap the GUI code in a class in which the event handlers are methods of the class and the widgets are the instance variables. Since methods have access to all an object's instance variables, we have neatly solved the problem.
Inspect and run the following code, which is a previous example re-written in an OO style:
from tkinter import * from tkinter.ttk import * class GreetingGui(object): def __init__(self, window): """Setup the label and button on given window""" self.window = window self.data = StringVar() self.hello_label = Label(self.window, textvariable=self.data) self.hello_label.grid(row=0, column=0) self.hello_button = Button(self.window, text="Say hello", command=self.say_hello) self.hello_button.grid(row=1, column=0) def say_hello(self): """The event handler for clicks on the button""" self.data.set("Hello World!") def main(): """Set up the GUI and run it.""" window = Tk() greeting_gui = GreetingGui(window) window.mainloop() main()
Note the following:
- The instance variables of the class, i.e. the variables like
self.data, define the GUI state.
- The methods of the class are the various event handlers (and support methods).
- All event handlers have access to the GUI state via the self parameter.
mainfunction creates the window and triggers the main loop.
Exercise: Counting in OO
Recreate the 'Two Function Increments' program from earlier that counts but encapsulate the GUI code in a class CounterGui. This program should have all the same functionality as the previous program: The program has two buttons and a label that displays the current value of a counter. One button, bearing the text +1, should add one to the counter, while the other button, labelled -1, should subtract one from it. The counter should start at zero.
Here is a template to get you started:
from tkinter import * from tkinter.ttk import * # Write your code here def main(): """Set up the GUI and run it""" window = Tk() counter_gui = CounterGui(window) window.mainloop() main()