GUI Development Basics in Python

In this tutorial we present the development of a GUI-based application in Python using tkinter. A variety of widgets are covered including: labels, entries, buttons, spinboxes, combo boxes, radio buttons, check buttons, scale and calendars. The focus for this tutorial is on constructing the view layer (how the application appears) with minimal development of the underlying logic.

Application Design

Before we write any code we need to design the layout for our application.
For this example we will divide our application into three main parts. The left frame will contain the entry (input) components of our application. The right frame will include additional configuration components in the form of radio buttons and checkbuttons.
The button frame will contain command buttons. An overview of the layout is shown below.

Step 1: The top-level window

We begin by creating the top-level window (root) for the application. Lines 1 and 2 of the code import the tkinter package, that contains all of the basic interface widgets, together with the ttk extension package, which includes enhanced and additional widgets.
The main control window of the application is created on line 5 of the code. The last line of the code (line 17) initialises the control loop for the GUI application – this line should always occur at the end of the code listing.

Lines 7-15 create frames for the three main parts of the application interface as explained above. To ensure that the button frame spans all of the application window we use the columnspan option.

from tkinter import *
from tkinter.ttk import *

# create the main application window
root = Tk()

# create a frame for a variety of entry and scale widgets
left_frame = Frame(root)
left_frame.grid(row=0, column=0)

right_frame = Frame(root)
right_frame.grid(row=0, column=1)

button_frame = Frame(root)
button_frame.grid(row=1, column=0, columnspan=2)

root.mainloop()

Step 2: Labels and Entries

In the next step we add two pairs of labels and entry widgets that will allow the user to enter their first name and last name.
These widgets are packed using the grid packing system within the left frame as shown in the diagram below. The grid consists of two rows and two columns, with the numbering starting at 0 for both the row and column.

Labels are added to the application using the Label widget as seen in line 2 below. The font command is used to change the appearance of the text, using a size larger than the default and representing the text using boldface. The Entry widget is used to get input from the user, in this case to get their first and last names. Both of the Entry widgets are associated with a text variable. When text is entered in the entry box the value of the text variable is updated automatically. Both of the text variables are mapped string variables.

The following code is added to the previous code after the creation of the left, right and button frames and before the final line of code.

# label and entry widgets used to enter information. Values linked to string variable.
first_name_label = Label(left_frame, text="First Name", font=("-size", 15, "-weight", "bold"))
first_name_label.grid(row=0, column=0, padx=5, pady=5)
first_name = StringVar()
first_name_entry = Entry(left_frame, textvariable=first_name)
first_name_entry.grid(row=0, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

last_name_label = Label(left_frame, text="Last Name", font=("-size", 15, "-weight", "bold"))
last_name_label.grid(row=1, column=0, ipady=5)
last_name = StringVar()
last_name_entry = Entry(left_frame, textvariable=last_name)
last_name_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

The resulting GUI after this code is run is shown below.

Step 3: Adding a command button

In this step we add a command button that will display the details that the user has entered using a message box.
The button is created using a Button widget. This is placed in the button frame that appears at the bottom of the screen. The Button widget command takes two parameters in this case. The first, text, sets the text that will be displayed in the button. The second, command, configures the function that will be called when the button is pressed.

print_button = Button(button_frame, text="Print details", command=print_details)
print_button.grid(row=0, column=0, padx=5, pady=10)

When the button is pressed a message box will be displayed showing the first and last name that has been entered in the two Entry boxes. To create a messagebox object an additional import command must be added to the top of the code file (just below the existing import commands).

The function print_details is defined which will be called when the button is pressed. The function prints the first and last names to the terminal output – this line of code is used for code development and debugging purposes and could be removed from the final version. Line 5 of the code below creates a new messagebox which will print the user’s first and last names in a pop-up window.

from tkinter import messagebox

def print_details():
    print(first_name.get(), last_name.get())
    messagebox.showinfo("Details", "hello " + str(first_name.get()) + " " + str(last_name.get()))

Step 4: Adding a spinbox

The next step involves adding a spinbox entry, which will allow users to enter a numeric value (their age) from a fixed selection of values. The spinbox entry is linked to an integer-valued variable by the textvariable option. Two further options also specify the minimum and maximum values that can be selected from. Note the use of an underscore at the from_ option – this is avoid a name class with the builtin from keyword.

age_label = Label(left_frame, text="Age", justify="center", font=("-size", 15, "-weight", "bold"))
age_label.grid(row=2, column=0, ipady=5)
age = IntVar()
age_entry = Spinbox(left_frame, textvariable=age, from_=0, to=100)
age_entry.grid(row=2, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

A screenshot of the resulting spin box is shown below.

Step 5: Adding a Combo box

Step 5 involves adding a Combo box to the application, which allows the user to select from a predefined list of choices. In this case the Combo box provides a list of colours that the user can choose from to pick their favourite colour.
The Combobox widget is linked to the colour string variable using the textvariable option. The values option links to the list used to store the available colours.

colour_label = Label(left_frame, text="Favourite Colour", justify="right", font=("-size", 15, "-weight", "bold"))
colour_label.grid(row=3, column=0, ipady=5)
colour = StringVar()
colour_list = [
    'green',
    'blue',
    'red',
    'yellow',
    'orange',
    'purple',
    'pink'
]
colour_entry = Combobox(left_frame, textvariable=colour, values=colour_list)
colour_entry.grid(row=3, column=1, padx=5, pady=(0, 10), sticky=tkinter.E)

The screenshot below shows the combo box being used to select the user’s favourite colour. When clicked on, a menu drops down from the entry boxv allowing the user to select from the available colours.

Step 6: Date entry

The DateEntry widget is used in this step to enter the user’s birthday. To access the DateEntry widget – together with its associated calendar widget, we need to import the tkcalendar package. This import code should be placed near the top of the file after the other import commands.

from tkcalendar import Calendar, DateEntry

The locale argument is configured so that the dates are displayed using Australian date formatting (en_AU). The year argument is used to set the year that the DateEntry will start at when it is first opened. Similar arguments are included for setting the initial day and the month. The date_pattern argument is used to specify the format that the dates will be displayed in. In this case the day and month will both be displayed using two digits, while the year will be displayed using four digits.

dob_label = Label(left_frame, text="Date of birth", justify="right", font=("-size", 15, "-weight", "bold"))
dob_label.grid(row=4, column=0)
dob_entry = DateEntry(left_frame, width=12, locale="en_AU", background='darkblue', foreground='white', borderwidth=2, year=2000, month=1, day=1, date_pattern = 'dd/mm/y')
dob_entry.grid(row=4, column=1, padx=5, sticky=tkinter.E)

The resulting date entry box is shown below.

Step 7: Using a scale widget

For step 7 we use a scale widget to introduce a slider that can be used to select a test score between 0 and 100. Like the Spinbox widget, the Scale widget includes options for setting the minimum and maximum values for the slide. The variable argument links the value of the slider to an integer valued variable. The command argument links the slider to a function that is called whenever the slider is moved. A Label widget is used to display the value of the test score – this will be updated when the slider is moved.

test_score_label = Label(left_frame, text="Test score", font=("-size", 15, "-weight", "bold"))
test_score_label.grid(row=5, column=0)
testScore = IntVar()
test_score_scale = Scale(left_frame, from_=0, to=100, orient=HORIZONTAL, variable=testScore, command=update_score_display)
test_score_scale.grid(row=5, column=1)
test_score_display = Label(left_frame, text="Score is 0")
test_score_display.grid(row=6, column=1)

The function update_score_display is called whenever the slider is moved. This function updates the text displayed in the test score display label by getting the current value of the test score variable. The function, as shown below, is placed after any import commands with other function definitions.

def update_score_display(event):
    test_score_display.config(text="Score is "+ str(testScore.get()))

A screenshot of the resulting slider is shown below.

Step 8: Radio buttons

Radio buttons allow the user to make a single selection from a predefined list of choices. The selection is made by choosing the appropriate button. For our application users will select their favourite pet by clicking the appropriate button.

The radio buttons are grouped together using a LabelFrame, which as its name suggests is a frame that includes a text label.
A string-valued variable is used to store the value of the favourite pet – this variable is then linked to each of the radio buttons.

fave_animal = StringVar()
pet_frame = LabelFrame(right_frame, text="Favourite Pet")
pet_frame.grid(row=0, column=0, padx=10)
dog_lover_label= Label(pet_frame, text="Dog Lover")
dog_lover_label.grid(row=0, column=0)
dog_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="dog")
dog_lover_select.grid(row=0, column=1)
cat_lover_label = Label(pet_frame, text="Cat Lover")
cat_lover_label.grid(row=1, column=0)
cat_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="cat")
cat_lover_select.grid(row=1, column=1)
rabbit_lover_label = Label(pet_frame, text="Rabbit Lover")
rabbit_lover_label.grid(row=2, column=0)
rabbit_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="rabbit")
rabbit_lover_select.grid(row=2, column=1)

The resulting label frame containing three labelled radio buttons is shown below.

Step 9: Adding check buttons

In this step check buttons are used to allow the user to select zero or more showbags. The check buttons are grouped together in a labelled frame and placed below the radio buttons frame. For each option there is an associated Boolean-valued variable that switches between true and false, with the default value in this case being false. Checkbutton widgets include a text label and a variable option which associates with the corresponding Boolean-valued variable.

showbag_frame = LabelFrame(right_frame, text="Showbag Order")
showbag_frame.grid(row=1, column=0, padx=10, pady=10)

warheads = BooleanVar()
warheads_select = Checkbutton(showbag_frame, text="Warheads", variable=warheads)
warheads_select.grid(row=0, column=0, sticky=W)

freddo = BooleanVar()
freddo_select = Checkbutton(showbag_frame, text="Freddo", variable=freddo)
freddo_select.grid(row=1, column=0, sticky=W)

blues_clues = BooleanVar()
blues_clues_select = Checkbutton(showbag_frame, text="Blue's Clues", variable=blues_clues)
blues_clues_select.grid(row=2, column=0, sticky=W)

Final application

A screenshot of the final application is shown below.

Below is a full code listing.

import tkinter as tk
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
from tkcalendar import Calendar, DateEntry


def print_details():
    print(first_name.get(), last_name.get())
    messagebox.showinfo("Details", "hello " + str(first_name.get()) + " " + str(last_name.get()))

def update_score_display(event):
    test_score_display.config(text="Score is "+ str(testScore.get()))
# create the main application window
root = Tk()

# create a frame for a variety of entry and scale widgets
left_frame = Frame(root)
left_frame.grid(row=0, column=0)

right_frame = Frame(root)
right_frame.grid(row=0, column=1, sticky=N)

button_frame = Frame(root)
button_frame.grid(row=1, column=0, columnspan=2)
# label and entry widgets used to enter information. Values linked to string variable.
first_name_label = Label(left_frame, text="First Name", justify="center", font=("-size", 15, "-weight", "bold"))
first_name_label.grid(row=0, column=0, padx=5, pady=5)
first_name = StringVar()
first_name_entry = Entry(left_frame, textvariable=first_name)
first_name_entry.grid(row=0, column=1, padx=5, pady=(0, 10), sticky=tk.E)

last_name_label = Label(left_frame, text="Last Name", justify="center", font=("-size", 15, "-weight", "bold"))
last_name_label.grid(row=1, column=0, ipady=5)
last_name = StringVar()
last_name_entry = Entry(left_frame, textvariable=last_name)
last_name_entry.grid(row=1, column=1, padx=5, pady=(0, 10), sticky=tk.E)


print_button = Button(button_frame, text="Print details", command=print_details)
print_button.grid(row=0, column=0, padx=5, pady=10)

age_label = Label(left_frame, text="Age", justify="center", font=("-size", 15, "-weight", "bold"))
age_label.grid(row=2, column=0, ipady=5)
age = IntVar()
age_entry = Spinbox(left_frame, textvariable=age, from_=0, to=100)
age_entry.grid(row=2, column=1, padx=5, pady=(0, 10), sticky=tk.E)

colour_label = Label(left_frame, text="Favourite Colour", justify="right", font=("-size", 15, "-weight", "bold"))
colour_label.grid(row=3, column=0, ipady=5)
colour = StringVar()
colour_list = [
    'green',
    'blue',
    'red',
    'yellow',
    'orange',
    'purple',
    'pink'
]
colour_entry = Combobox(left_frame, textvariable=colour, values=colour_list)
colour_entry.grid(row=3, column=1, padx=5, pady=(0, 10), sticky=tk.E)

dob_label = Label(left_frame, text="Date of birth", justify="right", font=("-size", 15, "-weight", "bold"))
dob_label.grid(row=4, column=0)
dob_entry = DateEntry(left_frame, width=12, locale="en_AU", background='darkblue', foreground='white', borderwidth=2, year=2000)

dob_entry.grid(row=4, column=1, padx=5, sticky=tk.E)

test_score_label = Label(left_frame, text="Test score", font=("-size", 15, "-weight", "bold"))
test_score_label.grid(row=5, column=0)
testScore = IntVar()
test_score_scale = Scale(left_frame, from_=0, to=100, orient=HORIZONTAL, variable=testScore, command=update_score_display)
test_score_scale.grid(row=5, column=1)
test_score_display = Label(left_frame, text="Score is 0")
test_score_display.grid(row=6, column=1)

s = Style()
s.configure('TLabelframe.Label', font=("-size", 15, "-weight", "bold"))

fave_animal = StringVar()
pet_frame = LabelFrame(right_frame, text="Favourite Pet")
pet_frame.grid(row=0, column=0, padx=10)
dog_lover_label= Label(pet_frame, text="Dog Lover")
dog_lover_label.grid(row=0, column=0)
dog_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="dog")
dog_lover_select.grid(row=0, column=1)
cat_lover_label = Label(pet_frame, text="Cat Lover")
cat_lover_label.grid(row=1, column=0)
cat_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="cat")
cat_lover_select.grid(row=1, column=1)
rabbit_lover_label = Label(pet_frame, text="Rabbit Lover")
rabbit_lover_label.grid(row=2, column=0)
rabbit_lover_select = Radiobutton(pet_frame, textvariable=fave_animal, value="rabbit")
rabbit_lover_select.grid(row=2, column=1)

showbag_frame = LabelFrame(right_frame, text="Showbag Order")
showbag_frame.grid(row=1, column=0, padx=10, pady=10)

warheads = BooleanVar()
warheads_select = Checkbutton(showbag_frame, text="Warheads", variable=warheads)
warheads_select.grid(row=0, column=0, sticky=W)

freddo = BooleanVar()
freddo_select = Checkbutton(showbag_frame, text="Freddo", variable=freddo)
freddo_select.grid(row=1, column=0, sticky=W)

blues_clues = BooleanVar()
blues_clues_select = Checkbutton(showbag_frame, text="Blue's Clues", variable=blues_clues)
blues_clues_select.grid(row=2, column=0, sticky=W)

root.mainloop()

Creating a dice game in Python (part 3)

In this post we continue developing a dice game in Python.

  • In Iteration 1 we coded a single round of the dice game for one player.
  • In Iteration 2 we added an additional loop so that multiple rounds were completed until the target score is reached.

Iteration 3

For this iteration we add a second player to the game. A high-level design for the revised game is shown below.

set target
set score1 to 0
set score2 to 0

while score1 < target and score2 < target
   player1 completes their turn
   update score1
   player2 completes their turn
   update score2
end while

output winner and scores

In the algorithm above, if player 1 reaches the target score, then player 2 will also get a turn before the loop terminates. However, the game should terminate as soon as one player reaches the target score. Another issue that we need to consider is ensuring that the amount of repeated code is minimised. A literal interpretation of the pseudocode above can lead to repeated code which we should avoid.

We improve the algorithm by adding a current player counter to keep track of who the current player is.

set target
set score1 to 0
set score2 to 0
set current_player to 1

while score1 < target and score2 < target
   current_player completes their turn
   if current_player = 1
   then
      update score1
      set current_player to 2
   else
      update score2
      set current_player to 1
   end if
end while

output winner and scores

After refining the algorithm to reduce the amount of repeated code we implement the algorithm in Python.
The first part of the code, as shown below, includes the necessary package imports together with variable initialisations. The variable current_player is used to track which player has the current turn. Initially this has a value of 1 (for player one) and will alternate between 1 and 2 until the game is complete.

import random
import time

target = 200
score1 = 0
score2 = 0
current_player = 1

The next block of code contains the main control loop and code for running the current player's round. The condition for the control loop (highlighted in the code listing) checks whether both scores are below the target score. The next line of code provides an indication of who the current player is.

while score1 < target and score2 < target:
    print("Player", current_player, "turn")
    round_score = 0
    valid = True
    while valid:
        roll1 = random.randint(1, 6)
        roll2 = random.randint(1, 6)
        total = roll1 + roll2
        if total == 7:
            valid = False
            print(total)
        else:
            round_score = round_score + total
            print(total, end=", ")
    print("You scored", round_score, "this round")

The block of code starting at line 23 is responsible for printing the total score of the current player and then updating the value of the current player before the while loop runs again. The code includes a conditional statement that checks who the current player is. Note that the code in the two branches of the condition block are very similar. This provides further opportunities for improving (refactoring) the code to reduce repetition. This is explore in the next part of the tutorial where we generalise to any number of players.

    if current_player == 1:
        score1 = score1 + round_score
        print("Total score for player 1 is", score1)
        current_player = 2
    else:
        score2 = score2 + round_score
        print("Total score for player 2 is", score2)
        current_player = 1
    time.sleep(1)

The final block of code outputs the winner of the game together with the final score. The winner is determined by comparing the scores of the two players within an if statement condition.

if score1 > score2:
    print("Player 1 wins", score1, "to", score2)
else:
    print("Player 2 wins", score2, "to", score1)

Example output that is generated when the code is run is shown below. Note that for brevity the output has been elided with only the first three and last round shown.

Player 1 turn
9, 10, 11, 6, 7
You scored 36 this round
Total score for player 1 is 36
Player 2 turn
2, 8, 10, 7
You scored 20 this round
Total score for player 2 is 20
Player 1 turn
6, 5, 9, 6, 9, 11, 10, 6, 9, 7
You scored 71 this round
...
Player 2 turn
11, 8, 11, 4, 6, 6, 5, 10, 5, 9, 7
You scored 75 this round
Total score for player 2 is 234
Player 2 wins 234 to 154

Creating a dice game in Python (part 2)

Iteration 2

In the second iteration of the development of Seven Out we add an outer loop so that the game will continue until the player’s total score equals or exceeds the target score – in this case 200. In the pseudocode shown below the outer loop is modelled as a while loop with a condition to check whether the target score has been met. The algorithm used inside of the outer loop is the same as that used in iteration 1 except a new variable is used to keep track of the score for the current round.


set target to 200
set score to 0
while score < target
   set round_score to 0
   loop
      set total to sum of two random integers between 1 and 6
      if total = 7
      then
         exit loop
      else
         add total to round_score
      end if
    end loop
    output round_score
end while
output score

The code written for this iteration is similar to that from iteration 1. New lines of code are highlighted in the listing below. Line 6 contains a while loop with a condition that checks that the score is less than the target. The while loop will continue while
this condition is true. In particular this while loop will always run at least once. Line 7 creates a new variable to represent the score for the current round - the value for this variable is reset to zero each time the outer loop is repeated. Line 17 updates the round score by adding the current dice total. Note that when a total of 7 is rolled this is not added to the round score.

import random

target = 200
score = 0

while score < target:
    round_score = 0
    valid = True
    while valid:
        roll1 = random.randint(1, 6)
        roll2 = random.randint(1, 6)
        total = roll1 + roll2
        if total == 7:
            valid = False
            print(total)
        else:
            round_score = round_score + total
            print(total, end=", ")
    score = score + round_score
    print("You scored", round_score, "this round")
    print("Total score is", score)

print("Well done. You got to", target)

Line 18 of the code uses an option within the print statement to alter its behaviour. The end option will by default print a newline character at the end of the output, meaning that subsequent output will appear on a new line. In this case we replace the default behaviour and instead end with a comma followed by a space. This means all of the totals for a round will be printed out on one line.

Output generated by running the game is shown below. For each round a list of comma-separated dice totals are shown, followed by the total for that round and then the total score. At the end of the game the player's total is shown.

8, 2, 3, 8, 2, 4, 9, 7
You scored 36 this round
Total score is 36
9, 5, 8, 10, 7
You scored 32 this round
Total score is 68
7
You scored 0 this round
Total score is 68
2, 9, 5, 11, 4, 4, 10, 6, 11, 11, 3, 4, 7
You scored 80 this round
Total score is 148
3, 6, 9, 7
You scored 18 this round
Total score is 166
6, 5, 5, 4, 7
You scored 20 this round
Total score is 186
8, 6, 4, 3, 6, 6, 7
You scored 33 this round
Total score is 219
Well done. You got to 200

Creating a dice game in Python (part 1)

Introduction

In this series of posts we will look at how to code a simple dice game in Python, starting with the initial concept, all the way through to the coding and adding extension features.


The game that we will develop is a simple two dice game called Sevens out. The game rules from lovetoknow.com are as follows:

Game rules

  1. The players decide upon a winning score, this could be 500 or 1,000.
  2. Each player, in turn, throws the dice.
  3. Each player rolls and continues to roll the dice until they throw a 7.
  4. They score the sum of all the numbers thrown before the 7.
  5. Players announce their tally after each throw and once they have thrown a 7, their score is noted on the score sheet.
  6. Doubles score double the value. For example, if a player who throws two 4s making eight, their score is 16.
  7. The player reaches the pre-arranged total first is the winner.

Iteration 1

Rather than attempting to develop the entire game in one pass, we will develop the game in multiple stages using an iterative development approach.
For the first iteration we will get one round of dice rolling completed for a single player.

Before writing any code we will develop a high-level design using pseudocode. This allows us to consider the key logical parts of the algorithmic solution without having to get bogged downed in the finer details of exactly how to code this in Python.

The pseudocode for our proposed first iteration is shown below. The most important part of the pseudocode is that it conveys the intended logic of the problem we are solving. For example the third and fourth lines of pseudocode indicates that a random number between 1 and 6 needs to be generated. How this is done does not matter at this stage – indeed the solution is dependent on the coding language being used, whereas pseudocode should be coding language independent.


set score to 0
loop
   set roll1 to random number between 1 and 6
   set roll2 to random number between 1 and 6
   set total to roll1 + roll2
   if total = 7
   then
      exit the loop
   else
      set score to score + total
   end if
end loop
output score

The resulting code in Python is almost a line for line translation of the pseudocode. One noteworthy change is the addition of a Boolean-valued variable, valid, which is used to indicate whether all rolls to date are valid (do not add up to 7). The while loop will continue whilst this variable is true. When a total of 7 is rolled the value of the variable becomes false. The use of this loop termination variable is highlighted in the code below. The relationship between the remainder of the code and the pseudocode should hopefully be evident.

import random

valid = True
score = 0

while valid:
    roll1 = random.randint(1, 6)
    roll2 = random.randint(1, 6)
    total = roll1 + roll2
    if total == 7:
        valid = False
    else:
        score = score + total

print("Score is", score)

The screenshot below shows the code running in the Pycharm IDE. The code runs until a total of 7 is rolled and then prints the resulting score.