2.3 Graphical User Interface (GUI) tkinter

In the exercises we have considered so far, the execution of the program has been predetermined by the code we wrote. We now introduce the concept of Event-Driven Programming. For example, we may want to execute a certain function when the user clicks on a button. tkinter (tk interactive) allows us to monitor for the occurrence of an event

The tkinter Window

2.3.1 Creating a Window

The highest level of tkinter objects is the window. The following program begins by importing tkinter.

line 6: We use tk.Tk() to create an INSTANCE of a tk window named win. This is the top level object in tkinter. You can use any variable name you wish for the window. It could be win, window, frank, root, etc. Let's check to see to what class win belongs . Here Tk is the class of tkinter windows Tk. Notice that as convention dictates, classes are capitalized.

Lines 8-9: establish properties of win. The window will be 750x270 pixels, and the upper right corner will be located 300 pixels from the left side of the screen and 400 pixels from the top of the screen.

Line 12: creates the window title. Finally my_window.mainloop() starts tkinter looking for events occurring in that window. Of course, since the window does not contain any buttons or other interactive elements, this infinite loop never detects an event.

There are many colors that you can specify by color name, HEX Code, or RGB Code. https://htmlcolorcodes.com/color-names/

In [9]:
#Ex 2.3.1
import tkinter as tk

# create an instance of a window object (named win)
win = tk.Tk()
print(type(win)) # confirms this to be a Tk object

#Set the geometry and backgroun color of the window win
win.geometry("750x270+300+400")
win.configure(bg="lightsalmon")

#Set the Title of Tkinter window
win.title("my window")

# begins the infinite loop to monitor events
win.mainloop()
<class 'tkinter.Tk'>

2.3.2 Displaying text in a window

This example is largely the same as Ex 2.3.1. In line 15 we create an instance of the class Label named my_label, specifying the text to be written in the label object, the background color, and the forground color (e.g. the color of the text).

There is one other difference. Now that we have created the label object, what is the relationship between this object and our Tk object named win. Line 17 directs the program to pack my label into win. It is positioned by default to top center.

line 21 is commented out. This is an alternative to placing my_label in win. Remove the # from 21 and place # at the start of 18. my_label will now have a relative position in win at 50% of the x and y dimensions of the window, and center justified, at that point. If you resize the window, the relatie position will remain the same.

my_label.place(relx=.5, rely=.5, anchor = 'center')

In [10]:
#Ex 2.3.2
import tkinter as tk

# create a window object (named win)
win = tk.Tk()

#Set the geometry and backgroun color of the window win
win.geometry("750x270+300+400")
win.configure(bg="lightsalmon")

#Set the Title of Tkinter window
win.title("my window")

# Create an instance of the wiget Label
my_label=tk.Label(win, text="Hello World!",bg='yellow',fg='black')

# Packs my_label into the defined window win with default positions: top/center
my_label.pack()

# Places my_label at a relative position in the win 50% of the x and y dimensions of the window, and center justified at that point
#my_label.place(relx=.5, rely=.5, anchor = 'center')

win.mainloop()

Ex 2.3.3 Display Both a Picture and Text in a Window

In this example, we will create two labels named label1 and caption. The first will contain a png (Portable Network Graphic) image.
Line9: Here we create a PhotoImage object which with file= pathname.

Lines10-11 We create a Label object named label1 and caption. The argument to create label1 specifies that the label will contain the image named pic, while the label caption contains text.

lines 14-15 packs the label1 and caption into root.

Experiment: What happens if we reverse the order of packing?

In [14]:
#Ex 2.3.3
import tkinter as tk 
root = tk.Tk()  
root.geometry=("200x200")
root.title("A pretty rose")
root.configure(bg="lightsalmon")

# We instantiate a tk.PhotoImage object
pic=tk.PhotoImage(file='py2.3images/py2.3rose.png')
label1=tk.Label(image=pic)
caption=tk.Label(text="Jean Giono rose")

# Packing the two labels into win
label1.pack()
caption.pack()

root.mainloop()

Ex 2.3.4.1 Frames and pack()

A frame is a container in which you may embed other tkinter objects.
lines 7-9 In this example, we are not embedding anything in the frames. We will differentiate among the three frames by giving them different background colors and different sizes. lines 11-13 Here we pack the frames into their designated masters, defined when we created the frames. By default, packing objects with no position specified will place them one on top of the other.
Experiment

  1. Change the order of lines 11,12, and 13. What happens?
  2. Resize the window root (make it larger and smaller) What happens?
In [22]:
#Ex 2.3.4.1
import tkinter as tk 
root = tk.Tk()  
root.title("root")
root.configure(bg="lightsalmon")
# create three frames inside root
frame1=tk.Frame(master=root,height=200, width=200, bg='red')
frame2=tk.Frame(master=root,height=100, width=100, bg='blue')
frame3=tk.Frame(master=root,height=50, width=50, bg='green')
# pack the frames 
frame1.pack()
frame2.pack()
frame3.pack()
root.mainloop()

2.3.4.2 Positioning the pack()

The method pack() has an optional argument side. This can take on values: tk.TOP. tk.BOTTOM, tk.LEFT, and tk.RIGHT . The only change made in the following code is that the side argument has been added as tk.LEFT. That means the frames will stack from the left. Clearly, the default is side=tk.TOP.
experiment

  1. Use different side arguments in packing the frames. Explain.
  2. Resize the window root
In [25]:
#Ex 2.3.4.2
import tkinter as tk 
root = tk.Tk()  
root.title("root")
root.configure(bg="lightsalmon")
# create three frames inside root
frame1=tk.Frame(master=root,height=200, width=200, bg='red')
frame2=tk.Frame(master=root,height=100, width=100, bg='blue')
frame3=tk.Frame(master=root,height=50, width=50, bg='green')
# pack the frames 
frame1.pack(side=tk.LEFT)
frame2.pack(side=tk.LEFT)
frame3.pack(side=tk.LEFT)
root.mainloop()

2.3.5 Structuring the window

If we want to pack a number of items into a Tk window, and we don't want them simply stacked on each other, the work involved in placing them at specific locations in the window can be problematical. We can address this by placing a grid on the window that allows us to address areas by referencing a row and column of the grid, and further placing frames or other widgets in the cells of the grid.

Ex 2.3.4 Grids

The method .grid() is another tool to manage the geometry of tkinter objects. CAUTION:You cannot use both pack and grid on widgets that have the same master. We can attach a grid to a window. In Ex 2.3.4 we create a window named root. A frame is like a container which can hold other widgets, although in this problem each frame we instantiate is empty and has a different background color.

Line 12 creates an object called frame from the class tk.Frame(). The frame has a height and width as specified. and a background color bg=rainbow[n]. Those colors are defined in the array in line 6 We will index this array starting at n=0 (that would be 'red'.

Lines 10-14 Nested for loops iterate thru rows (i) and columns (j). For each row, column pair, we instantiate an object named frame from the class tk.Frame. The arguments describe what object will hold the frame (master), the height and width of the frame, and the background color bg=rainbow[n]. At the end of the for loop nest, we increment n by 1 (n+=1)

line 13 Inside the nested for loop the frame just defined is positioned within the grid using frame.grid(row=i, column=j). This statement REPLACES THE .pack() we have used previously. It allows us to place this frame at a specific location in the grid.

Remember frame is the NAME OF THE OBJECT while tk.Frame is the class of these objects. We don't have to define frame1, frame2,...frame9. Each time we pass thru the loop we redefine the variable frame. It has a different color and is placed at a different position in the grid.

In [27]:
# Ex2.3.4
import tkinter as tk
root = tk.Tk()
root.title("root")

# Defining an array of colors and initializing the color index n=0
rainbow=['red','orangered','orange','yellow','green','blue','indigo','darkviolet','black']
n=0

# Step through the 3x3 grid
for i in range(3):
    for j in range(3):
        frame = tk.Frame(master=root,height=100, width=100, bg=rainbow[n])
        frame.grid(row=i, column=j)
        n+=1
root.mainloop()

Events and Actions

So far we have only talked about layouts of static elements. The power of a GUI is the ability to interact with the program. There are several widgets that provide that capability. We will begin with introducing the button. We are all familiar with the notion of a button in a web browser which takes the user to a different url. The tkinter button has the ability to do much more than this. Let's begin with creating a tk window and placing some widgets including a button in that window.

Ex2.3.5 A Simple Button

Specification for program We want to create a program which displays a spectrum of colors. The GUI has a button to build a 3x3 matrix of colors from a list of defined values. It also has a button, which when depressed will shuffle the colors to create a new array on the display.

The Design
We propose to use two primary frames: frame1 and frame2 embedded in the root window. The frame1 will contain a button which when pressed will change the order of the colors which we will grid in frame2. That matrix of colors is identical to that which we programmed in Ex 2.3.4 except the the scframe are placed in a grid within frame2. See Note1 below to see restrictions on using grid.

lines 24-26 In this segment of the program we use the frame constructor tk.Frame(master=root, height=50, width=300, bg='silver'). to create an instance of frame1. Secondly, we create a button named b using the button constructor tk.Button(master=frame1,bg="silver",text='Change Colors?',font=("Comic SansMS",20,"bold"),command=clik). Two things to note here. master=frame1 defines the fact that this button will be embedded in frame1. In the argument list we include command=clik. This states that when the button is pressed, we will execute the function click(). IT IS IMPORTANT TO NOTE THAT WHILE THE FUNCTION HAS PARENTHESES TO INDICATE AN ARGUMENT--WE ONLY PASS THE NAME OF THE FUNCTION clik not clik() In fact we will shortly find out that in order to pass arguments in the function command= we will have to adopt a slightly different approach. Finally, b.pack() will pack the button into the object named as the master in the button instance--frame1.

line 27 This constructs frame2 from the class of Frames. The master='root' and it is 300x300 pixels in size. This frame will enclose a 3x3 grid of frames named cframe, each sized to be 100x100 (yet to be defined).

Lines 30-34, We use nested for loops to create a 9 instances of fcell. Notice that the background color is set bg=rainbow[3*i+j]. Recall in the for loop i will count 3 iterations from 0 --0,1,2. Likewise, j will count 3 iterations over the same values, giving a total of 9 times fcell is created, each time with a different background color. It is important to note that we are using the SAME NAME fcell for each of these instances. Thus, if we want to refer back to them individually in the future, we will have to somehow store them. This is what we do in line 34. We start with an empty list (line 7), and each time we create an instance of fcell, we append it to the list cell. After exiting the nested for loops, there will be 9 individual records of the the nine cells in the grid in the 9 elements of the list cell.

Lines11-15 This is the definition for the function clik(). lines 6-7 declare rainbow (the list of colors used as backgrounds, and the cell, the list of the nine instances of fcell to be global variables. Initially cell was defined as an empty list and rainbow was given its initial nine values in the main program. The list cell was appended each time an fcell was created. By making these variables global, we can access them in both main and the clik function. Generally, using global variables is not recommended. The point of limiting the scope of definition of a variable is to avoid changing that variable in a function and inadvertently changing a variable of the same name elsewhere. Again, later we will learn how to pass arguments to functions executed when buttons are clicked.

Upon running the program the window root is created, and the various frames are embedded. Try clicking the button Change Color?. You may need to click on the window title frame first to bring it to the front. Each time you click the button, the array of colors will shift.

Note1: You cannot use both pack and grid on widgets that have the same master. The first one will adjust the size of the widget. The other will see the change, and resize everything to fit its own constraints. The first will see these changes and resize everything again to fit its constraints. The other will see the changes, and so on ad infinitum. They will be stuck in an eternal struggle for supremacy.

https://stackoverflow.com/questions/17267140/python-pack-and-grid-methods-together

In [ ]:
# Ex2.3.5
import tkinter as tk
import random

# definition of global variables
rainbow=['red','orangered','orange','yellow','green','blue','indigo','darkviolet','black']
cell=[]

# function clik re-colors the gridded cells embedded in frame2
def clik():
    global rainbow,cell
    random.shuffle(rainbow)
    for k in range(8):
        cell[k].config(bg=rainbow[k])
    return

# the main program
root = tk.Tk()
root.geometry('300x400')
root.configure(bg='silver')
root.title('root')

# Defining the two primary frames embedded in root as well as the button that activates function clik()
frame1=tk.Frame(master=root, height=50, width=300, bg='silver')
b=tk.Button(master=frame1,bg="silver",text='Change Colors?',font=("Comic SansMS",20,"bold"),command=clik)
b.pack()
frame2=tk.Frame(master=root,height=300, width=300)

# creating cells that will fill a 3x3 grid embedded in frame2
for i in range(3):
    for j in range(3):
        fcell= tk.Frame(master=frame2,height=100, width=100, bg=rainbow[3*i+j])
        fcell.grid(row=i, column=j)
        cell.append(fcell)

# packing frame1 and frame2 into root
frame1.pack()
frame2.pack()

#start mainloop()
root.mainloop()
In [1]:
# Ex 2.3.6: import from libraries
import tkinter as tk
from functools import partial

# define window for game
win=tk.Tk()
win.geometry('800x800+50+50')
win.title("Tic-Tac-Toe")

# initialization of variables
but=[]                     
play="X"

# Definition of functions used in the program
def click(but,ir,ic):
    global play
    kk=3*ir+ic
    if(but[kk].cget('text')==""):
        but[kk].config(text=play)

# is there a winner filling a row?        
    for kr in range (3):
        if but[3*kr].cget('text')!="" and but[3*kr].cget('text')==but[3*kr+1].cget('text') and but[3*kr+1].cget('text')==but[3*kr+2].cget('text'):
            print(but[3*kr].cget('text')," wins")
            for j in range (3):                   
                but[3*kr+j].config(bg='#ff0000')

# is there a winner filling a column?                
    for kc in range (3):
        if but[kc].cget('text')!="" and but[kc].cget('text')==but[kc+3].cget('text') and but[kc+3].cget('text')==but[kr+6].cget('text'):
            print(but[kc].cget('text')," wins")
            for j in range (3):                   
                but[kc+3*j].config(bg='#ff0000')

# is there a winner filling the diagonal 0,4,8
    if but[0].cget('text')!="" and but[0].cget('text')==but[4].cget('text') and but[4].cget('text')==but[8].cget('text'):        
        print(but[0].cget('text')," wins")
        for j in (0,4,8):                   
            but[j].config(bg='#ff0000')                                                               

# is there a winner filling the diagonal 2,4,6
    if but[2].cget('text')!="" and but[2].cget('text')==but[4].cget('text') and but[4].cget('text')==but[6].cget('text'):        
        print(but[2].cget('text')," wins")
        for j in (2,4,6):                   
            but[j].config(bg='#ff0000')              

# change player            
    if (play=="X"): play="O"
    else: play="X"
    return

# GUI buttons used in the game
for kr in range (3):
    for kc in range (3):
        kk=3*kr+kc
        bb=Button(win,text="",font=("Helvetica",20),height=150, width=150,bg="white", command=partial(click,but,kr,kc))
        bb.grid(row=kr, column=kc)
        but.append(bb)

# The Main Loop
win.mainloop()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-29178bc29ccd> in <module>
     54     for kc in range (3):
     55         kk=3*kr+kc
---> 56         bb=Button(win,text="",font=("Helvetica",20),height=150, width=150,bg="white", command=partial(click,but,kr,kc))
     57         bb.grid(row=kr, column=kc)
     58         but.append(bb)

NameError: name 'Button' is not defined

Ex2.3.7 Interaction Using the Keyboard

The following example is taken from Real Python

In [11]:
#Ex 2.3.7
import tkinter as tk

def fahrenheit_to_celsius():
    """Convert the value for Fahrenheit to Celsius and insert the
    result into lbl_result.
    """
    fahrenheit = ent_temperature.get()
    celsius = (5 / 9) * (float(fahrenheit) - 32)
    lbl_result["text"] = f"{round(celsius, 2)} \N{DEGREE CELSIUS}"

# Set up the window
window = tk.Tk()
window.title("Temperature Converter")
window.resizable(width=False, height=False)

# Create the Fahrenheit entry frame with an Entry
# widget and label in it
frm_entry = tk.Frame(master=window)
ent_temperature = tk.Entry(master=frm_entry, width=10)
lbl_temp = tk.Label(master=frm_entry, text="\u03B1 F")

# Layout the temperature Entry and Label in frm_entry
# using the .grid() geometry manager
ent_temperature.grid(row=0, column=0, sticky="e")
lbl_temp.grid(row=0, column=1, sticky="w")

# Create the conversion Button and result display Label
btn_convert = tk.Button(
    master=window,
    text="CONVERT",
    command=fahrenheit_to_celsius
)
lbl_result = tk.Label(master=window, text="\u03B1 C")

# Set up the layout using the .grid() geometry manager
frm_entry.grid(row=0, column=0, padx=10)
btn_convert.grid(row=0, column=1, pady=10)
lbl_result.grid(row=0, column=2, padx=10)

# Run the application
window.mainloop()

Ex 2.3.8 The Canvas Widget

In [ ]: