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

Measurement Toolkit (part 1)

Introduction

In this series of tutorials we explain how to develop a Measurement toolkit which provides functions for converting units of measurement and calculating the surface area and volume of a range of 3d shapes.

A screenshot showing the Measurement toolkit is given below showing the unit conversion capabilities of the tool.

Unit conversions in the Measurement Toolkit

Initial setup

The starting point for the development of the Measurement Toolkit is to download the initial source code files for the Measurement Toolkit. This zip file should be downloaded and extracted to a suitable location. These files provide the basic skeletal code for the application. It includes graphical user interface (GUI) code, together with stubs for the functions that do the conversions and calculations.

The files are listed below, with full code listings that can be expanded and view. We will describe each of these files in more detail in later parts of this tutorial as required.

  • convert_units.py – will contain functions for carrying out unit conversions. Code is provided in this file for converting units of length, however several errors have been included which need to be found through testing and fixed. Stubs (incomplete functions) for converting area and volume units are also given.
  • def convert_length_unit(value,inunit, outunit):
        if (inunit==outunit):
            result=value
        elif (inunit=="m") & (outunit=="mm"):
            result=value*1000
        elif (inunit=="m") & (outunit=="cm"):
            result=value*10
        elif (inunit=="m") & (outunit=="km"):
            result=value*1000
        elif (inunit=="km") & (outunit=="mm"):
            result=value*1000000
        elif (inunit=="km") & (outunit=="cm"):
            result=value*10000
        elif (inunit=="km") & (outunit=="m"):
            result=value*1000
        elif (inunit=="cm") & (outunit=="mm"):
            result=value*100
        elif (inunit=="cm") & (outunit=="m"):
            result=value/100
        elif (inunit=="cm") & (outunit=="km"):
            result=value/100*1000
        elif (inunit=="mm") & (outunit=="cm"):
            result=value/10
        elif (inunit=="mm") & (outunit=="m"):
            result=value/1000
        elif (inunit=="mm") & (outunit=="km"):
            result=value/1000*1000
        return result
    
    def convert_area_unit(value,inunit,outunit):
        return 0
    
    def convert_volume_unit(value,inunit,outunit):
        return 0
    
  • convert_units_tester.py – will contain test cases to check the correctness of the unit conversion functions. Test cases have been written for converting from metres to other units. Using the same syntax you will be required to complete the other test cases.
  • import unittest
    from convert_units import *
    
    class TestConvertUnits(unittest.TestCase):
    
        def test_m_to_mm(self):
            self.assertEqual(convert_length_unit(0.23,"m","mm"),230)
        def test_m_to_cm(self):
            self.assertEqual(convert_length_unit(0.25,"m","cm"),25)
        def test_m_to_m(self):
            self.assertEqual(convert_length_unit(178,"m","m"),178)
        def test_m_to_km(self):
            self.assertEqual(convert_length_unit(19000,"m","km"),19)
    
    if __name__ == '__main__':
        unittest.main()
    
  • mathtoolkit.py – the main program file, containing of the GUI code and user configurable code allowing additional shapes to be added to the surface area and volume calculator parts of the tool
  • from tkinter import *
    from tkinter import ttk
    from convert_units import *
    from surface_area import *
    from volume import *
    from PIL import Image,ImageTk
    import math
    
    
    def convert_units_and_display():
        if (unitsvar.get()==1):
            converted=convert_length_unit(float(unitval.get()),funitvar.get(),tunitvar.get())
        elif (unitsvar.get()==2):
            converted=convert_area_unit(float(unitval.get()),funitvar.get(),tunitvar.get())
        elif (unitsvar.get()==3):
            converted=convert_volume_unit(float(unitval.get()),funitvar.get(),tunitvar.get())
        convertedval.set(str(converted))
        
    def change_unit_options():
        m1=foptnmenu['menu']
        m1.delete(0,END)
        m2=toptnmenu['menu']
        m2.delete(0,END)
        
        if (unitsvar.get()==2):
            newvalues=['mm2','cm2','m2','km2']
            funitvar.set("m2")
            tunitvar.set("m2")
            image=Image.open("Images/area_convert.png").resize((780,300))
            img=ImageTk.PhotoImage(image)
            cimage_label.configure(image=img)
            cimage_label.image=img
    
        elif(unitsvar.get()==3):
            newvalues=['mm3','cm3','m3','km3']
            funitvar.set("m3")
            tunitvar.set("m3")
            image=Image.open("Images/volume_convert.png").resize((780,300))
            img=ImageTk.PhotoImage(image)
            cimage_label.configure(image=img)
            cimage_label.image=img
        else:
            newvalues=['mm','cm','m','km']
            funitvar.set("m")
            tunitvar.set("m")
            image=Image.open("Images/length_convert.png").resize((780,300))
            img=ImageTk.PhotoImage(image)
            cimage_label.configure(image=img)
            cimage_label.image=img
    
        
        for val in newvalues:
            m1.add_command(label=val,command=lambda v=funitvar,l=val:funitvar.set(l))
            m2.add_command(label=val,command=lambda v=tunitvar,l=val:tunitvar.set(l))
    
    
    def display_shape_sa(self):
        sproperties=sa_shapes[shapevar.get()]
        refresh_surface_area(sproperties[0],
                             sproperties[1])
        
    
    def calc_surface_area():
        arglist=""
        for i in range(0,len(valarray)-1):
            arglist=arglist+"{0},".format(valarray[i].get())
        arglist=arglist+"{0}".format(valarray[len(valarray)-1].get())
        answer=(eval(sa_shapes[shapevar.get()][2]+"("+arglist+")"))
        sa_text.set("Surface Area of {0} is {1:.2f} units^2".format(shapevar.get(),answer))
        
        
    
    
    def refresh_surface_area(varlist,imageloc):
        valarray.clear()
        for l in range(0,len(varlabels)):
            varlabels[l].destroy()
        for l in range(0,len(varentries)):
            varentries[l].destroy()
        for i in range(0,len(varlist)):
            valarray.append(StringVar(main))
            lab=ttk.Label(vframe,text="{0}=".format(varlist[i]))
            lab.grid(row=i+1,column=0,pady=20,sticky=W)
            varlabels.append(lab)
            entry=ttk.Entry(vframe,width=20,textvariable=valarray[i])
            entry.grid(row=i+1,column=1,sticky=W)
            varentries.append(entry)
        image=Image.open(imageloc).resize((400,400))
        img=ImageTk.PhotoImage(image)
        image_label.configure(image=img)
        image_label.image=img
    
    def display_shape_vol(self):
        vproperties=vol_shapes[vshapevar.get()]
        refresh_volume(vproperties[0],
                        vproperties[1])
        
    
    def calc_volume():
        arglist=""
        for i in range(0,len(vvalarray)-1):
            arglist=arglist+"{0},".format(vvalarray[i].get())
        arglist=arglist+"{0}".format(vvalarray[len(vvalarray)-1].get())
        answer=(eval(vol_shapes[vshapevar.get()][2]+"("+arglist+")"))
        vol_text.set("Volume of {0} is {1:.2f} units^3".format(vshapevar.get(),answer))
        
        
    
    
    def refresh_volume(varlist,imageloc):
        vvalarray.clear()
        for l in range(0,len(vvarlabels)):
            vvarlabels[l].destroy()
        for l in range(0,len(vvarentries)):
            vvarentries[l].destroy()
        for i in range(0,len(varlist)):
            vvalarray.append(StringVar(main))
            lab=ttk.Label(vframe2,text="{0}=".format(varlist[i]))
            lab.grid(row=i+1,column=1,pady=20)
            vvarlabels.append(lab)
            entry=ttk.Entry(vframe2,width=20,textvariable=vvalarray[i])
            entry.grid(row=i+1,column=2)
            vvarentries.append(entry)
        image=Image.open(imageloc).resize((400,400))
        img=ImageTk.PhotoImage(image)
        vimage_label.configure(image=img)
        vimage_label.image=img
    ####################
    ## Top level GUI setup
    ######################
    
    main=Tk()
    main.title('Measurement Toolkit')
    main.geometry('800x600')
    rows=0
    
    main.grid_rowconfigure(0, weight=1)
    main.grid_columnconfigure(0, weight=1)
    
    style = ttk.Style(main)
    style.configure('TRadiobutton', font=('Helvetica', 12))
    style.configure('TNotebook.Tab', font=('Helvetica', 14), padding=5)
    style.configure('TButton', font=('Helvetica', 14), padding=10)
    style.configure('TLabel', font=('Helvetica', 12))
    style.configure('TEntry', font=('Helvetica', 12), padding=5)
    style.configure('TMenubutton', font=('Helvetica', 12), padding=5)
    
    nb=ttk.Notebook(main)
    
    
    
    nb.grid(row=0,sticky="nesw")
    
    
    
    page1=ttk.Frame(nb)
    nb.add(page1,text='Conversions')
    page2=ttk.Frame(nb)
    nb.add(page2,text='Surface Area')
    page3=ttk.Frame(nb)
    nb.add(page3,text='Volume')
    
    ##################
    ## Conversions
    ###################
    page1.rowconfigure(0,weight=2)
    page1.rowconfigure(1,weight=1)
    page1.columnconfigure(0,weight=1,uniform=1)
    unitsvar=IntVar()
    unitsvar.set(1)
    
    
    cframe=ttk.Frame(page1,relief=RAISED,padding=5)
    cframe.grid(row=0,column=0,sticky="nesw")
    cframe.columnconfigure((0,1,2,3,4),weight=1)
    cframe.rowconfigure(1,weight=1)
    cframe.rowconfigure((0,2),weight=1)
    
    
    cimage_label=ttk.Label(cframe)
    cimage_label.grid(row=1,column=0,columnspan=5,sticky="ew")
    image=Image.open("Images/length_convert.png").resize((780,300))
    img=ImageTk.PhotoImage(image)
    cimage_label.configure(image=img)
    cimage_label.image=img
    
    ttk.Radiobutton(cframe,text="Length",variable=unitsvar,value=1,command=change_unit_options).grid(row=0,column=0,pady=10)
    ttk.Radiobutton(cframe,text="Area",variable=unitsvar,value=2,command=change_unit_options).grid(row=0,column=1,pady=10)
    ttk.Radiobutton(cframe,text="Volume",variable=unitsvar,value=3,command=change_unit_options).grid(row=0,column=2,pady=10)
    
    funitvar=StringVar(main)
    tunitvar=StringVar(main)
    unitchoices=['mm','cm','m','km']
    
    
    
    
    unitval=StringVar(main)
    unitval.set("0")
    convertedval=StringVar(main)
    convertedval.set("0")
    ttk.Label(cframe,text='Is Equivalent To').grid(row=2,column=2,padx=5,pady=5)
    uentry=ttk.Entry(cframe,width=20,textvariable=unitval)
    uentry.grid(row=2,column=0,sticky='E')
    foptnmenu=ttk.OptionMenu(cframe,funitvar,'m',*unitchoices)
    foptnmenu.configure(width=10)
    foptnmenu.grid(row=2,column=1)
    convertLabel=ttk.Label(cframe,textvariable=convertedval,width=20)
    convertLabel.grid(row=2,column=3)
    toptnmenu=ttk.OptionMenu(cframe,tunitvar,'m',*unitchoices)
    toptnmenu.configure(width=10)
    toptnmenu.grid(row=2,column=4)
    convertButton=ttk.Button(page1,text="Convert",
                         command=convert_units_and_display)
    convertButton.grid(row=1,column=0)
    
    #################
    ## Surface Area
    ###################
    sa_shapes={}
    
    ##
    ## ADD NEW SHAPES HERE FOR SURFACE AREA
    ##
    sa_shapes['Cube']=[['l'],"Images/cube.png","calc_cube_sa"]
    sa_shapes['Rectangular Prism']=[['l','w','h'],"Images/rect_prism.png","calc_rprism_sa"]
    
    
    
    
    ##
    ## DO NOT EDIT BELOW HERE
    ##
    
    page2.rowconfigure(0,weight=1)
    page2.rowconfigure(1,weight=1)
    page2.rowconfigure(2,weight=1)
    page2.rowconfigure(3,weight=1)
    page2.columnconfigure(0,weight=1,uniform=1)
    
    shapechoices=sa_shapes.keys()
    shapevar=StringVar(main)
    shapevar.set('Cube')
    
    shapeoptnmenu=ttk.OptionMenu(page2,shapevar,"Cube",*shapechoices,command=display_shape_sa)
    shapeoptnmenu.configure(width=30)
    shapeoptnmenu.grid(row=0,column=0,sticky=W)
    vframe=ttk.Frame(page2,relief=RAISED,padding=10)
    vframe.grid(row=1,column=0,sticky=NSEW)
    
    
    
    
    ttk.Label(vframe,text="Variables").grid(row=0,column=1,padx=20)
    iframe=ttk.Frame(page2,relief=RAISED,padding=5)
    iframe.grid(row=1,column=1,sticky=NSEW)
    valarray=[]
    varlabels=[]
    varentries=[]
    image_label=ttk.Label(iframe,background='white')
    image_label.grid(row=0,column=0)
    display_shape_sa(main)
    
    
    sa_text=StringVar()
    sa_text.set("Press Calculate button")
    ttk.Label(page2,textvariable=sa_text).grid(row=2,column=0, columnspan=2, sticky=W)
    ttk.Button(page2,text="Calculate",command=calc_surface_area).grid(row=3,column=0,columnspan=2)
    
    
    ################
    ## Volume
    ################
    vol_shapes={}
    ##
    ## ADD NEW SHAPES HERE FOR VOLUME
    ##
    vol_shapes['Cube']=[['l'],"Images/cube.png","calc_cube_vol"]
    vol_shapes['Rectangular Prism']=[['l','w','h'],"Images/rect_prism.png","calc_rprism_vol"]
    
    ##
    ## DO NOT EDIT BELOW HERE
    ##
    page3.rowconfigure(0,weight=1)
    page3.rowconfigure(1,weight=1)
    page3.rowconfigure(2,weight=1)
    page3.rowconfigure(3,weight=1)
    page3.columnconfigure(0,weight=1,uniform=1)
    vshapechoices=vol_shapes.keys()
    vshapevar=StringVar(main)
    vshapevar.set('Cube')
    
    vshapeoptnmenu=ttk.OptionMenu(page3,vshapevar,"Cube",*vshapechoices,command=display_shape_vol)
    vshapeoptnmenu.configure(width=30)
    vshapeoptnmenu.grid(row=0,column=0,sticky=W)
    vframe2=ttk.Frame(page3,relief=RAISED)
    vframe2.grid(row=1,column=0,sticky=NSEW)
    
    ttk.Label(vframe2,text="Variables").grid(row=0,column=1,columnspan=2,padx=20)
    iframe2=ttk.Frame(page3,relief=RAISED,padding=5)
    iframe2.grid(row=1,column=1,sticky=NSEW)
    vvalarray=[]
    vvarlabels=[]
    vvarentries=[]
    vimage_label=ttk.Label(iframe2,background='white')
    vimage_label.grid(row=0,column=0)
    display_shape_vol(main)
    
    vol_text=StringVar()
    vol_text.set("Press Calculate button")
    ttk.Label(page3,textvariable=vol_text).grid(row=2,column=0, columnspan=2, sticky=W)
    ttk.Button(page3,text="Calculate",command=calc_volume).grid(row=3,column=0,columnspan=2)
    
    main.mainloop()
    
  • surface_area.py – will contain the functions for calculating the surface area of various 3d shapes
  • import math
    
    def calc_cube_sa(length):
        return 6*(length)**2
    
    def calc_rprism_sa(length,width,height):
        return 0
    
  • surface_area_tester.py – will contain test cases to check the correctness of the surface area calculation functions
  • import unittest
    from surface_area import *
    
    class TestSurfaceArea(unittest.TestCase):
        def test_calc_cube_sa(self):
            self.assertEqual(calc_cube_sa(5),150)
            self.assertAlmostEqual(calc_cube_sa(3.275),64.35,2)
    
    if __name__ == '__main__':
        unittest.main()
    
  • volume.py – will contain the functions for calculating the volume of various 3d shapes
  • import math
    
    def calc_cube_vol(l):
        return l**3
    
    def calc_rprism_vol(l,w,h):
        return 0
    
  • volume_tester.py – will contain test cases to check the correctness of the volume calculation functions
  • import unittest
    from volume import *
    
    class TestVolume(unittest.TestCase):
        def test_calc_cube_vol(self):
            self.assertEqual(calc_cube_vol(5),125)
    
    if __name__ == '__main__':
        unittest.main()
    
  • Images – stores the image files that will be used to represent the 3d shapes, together with the unit conversion diagrams

To run the program you will also need to install the pillow package in Python using the following command:

pip install pillow pip --trusted-host pypi.org --trusted-host files.pythonhosted.org