GUI Notes
GUI Notes
Here are the four lines of Python code required to create the resulting GUI:
import tkinter as tk #1
win = tk.Tk() #2
win.title("Python GUI") #3
win.mainloop() #4
In line 1, we import the built-in tkinter module and alias it as tk to simplify our Python code.
In line 2, we create an instance of the Tk class by calling its constructor.
In line 3, we use the instance variable of the class (win) to give our window a title via the title
property.
In line 4, we start the window's event loop by calling the mainloop method on the class instance win.
We are preventing the GUI from being resized.
import tkinter as tk # 1 imports
win = tk.Tk() # 2 Create instance
win.title("Python GUI") # 3 Add a title
win.resizable(70, 70) # 4 Disable resizing the GUI
win.mainloop() # 5 Start GUI
Resizable() is a method of the Tk() class and, by passing in (0, 0), we prevent the GUI from being
resized. If we pass in other values, we hard-code the x and y start up size of the GUI, but that won't
make it nonresizable.
Here are the lines of Python code required to add a label in the resulting GUI:
In order to add a Label widget to our GUI, we are importing the ttk module from tkinter. Please note
the two import statements.
# imports #1
import tkinter as tk #2
from tkinter import ttk #3
Add the following code just above win.mainloop() located at the bottom of code.
ttk.Label(win, text="A Label").grid(column=0, row=0) # 5
In line 3 of the above code, we are importing a separate module from tkinter. The ttk module has some
advanced widgets that make our GUI look great. In a sense, ttk is an extension within tkinter.
import tkinter as tk # 1 imports
from tkinter import ttk #2
win = tk.Tk() # 3 Create instance
win.title("Python GUI") # 4 Add a title
ttk.Label(win, text="A Label").grid(column=100, row=100) #5
win.mainloop() # 6 Start GUI
NOTE : ttk stands for 'themed tk". It improves our GUI look and feel.
We are also making use of the grid layout manager, which we'll explore in much more depth after
some time.
Note how our GUI suddenly got much smaller than in previous recipes. The reason why it became so
small is that we added a widget to our form. Without a widget, tkinter uses a default size. Adding a
widget causes optimization, which generally means using as little space as necessary to display the
widget(s).
If we make the text of the label longer, the GUI will expand automatically.
Here are the lines of Python code required to add a button in the resulting GUI:
We are adding a button that, when clicked, performs an action. In this recipe, we will update the label
we added in the previous recipe, as well as updating the text property of the button.
# Modify adding a Label #1
aLabel = ttk.Label(win, text="A Label") #2
aLabel.grid(column=0, row=0) #3
# Button Click Event Callback Function #4
def clickMe(): #5
action.configure(text="** I have been Clicked! **")
aLabel.configure(foreground='red')
# Adding a Button #6
action = ttk.Button(win, text="Click Me!", command=clickMe) # 7
action.grid(column=1, row=0) #8
In line 2 we are now assigning the label to a variable.
In line 3 we use this variable to position the label within the form. We will need this variable to change
its properties in the clickMe() function. By default, this is a module-level variable so we can access it
inside the function as long as we declare the variable above the function that calls it.
Line 5 is the event handler that is being invoked once the button gets clicked.
In line 7, we create the button and bind the command to the clickMe)function.
Here are the lines of Python code required to add a textbox in the resulting GUI:
In tkinter, the typical textbox widget is called Entry.
# Modified Button Click Function # 1
def clickMe(): # 2
action.configure(text='Hello ' + name.get())
# Position Button in second row, second column (zero-based)
action.grid(column=1, row=1)
# Changing our Label # 3
ttk.Label(win, text="Enter a name:").grid(column=0, row=0) # 4
# Adding a Textbox Entry widget # 5
name = tk.StringVar() # 6
nameEntered = ttk.Entry(win, width=12, textvariable=name) # 7
nameEntered.grid(column=0, row=1) # 8
Setting the focus to a widget and disabling widgets
All we have to do to set the focus to a specific control when the GUI appears is call the focus() method
on an instance of a tkinter widget
nameEntered.focus() # Place cursor into name Entry
We can also disable widgets. To do that, we set a property on the widget. We can make the button
disabled by adding this one line of Python code:
action.configure(state='disabled') # Disable the Button Widget
Here are the lines of Python code required to add a drop-down combo box in the resulting GUI:
ttk.Label(win, text="Choose a number:").grid(column=1, row=0) # 1
number = tk.StringVar() #2
numberChosen = ttk.Combobox(win, width=12, textvariable=number) #3
numberChosen['values'] = (1, 2, 4, 42, 100) #4
numberChosen.grid(column=1, row=1) #5
numberChosen.current(0) #6
If we want to restrict the user to only be able to select the values we have programmed into the
Combobox, we can do that by passing the state property into the constructor. Modify line 3 in the
previous code to:
numberChosen = ttk.Combobox(win, width=12, textvariable=number, state='readonly')
Now users can no longer type values into the Combobox. We can display the value chosen by the user
by adding the following line of code to our Button Click Event Callback function:
# Modified Button Click Callback Function
def clickMe():
action.configure(text='Hello ' + name.get()+ ' ' +numberChosen.get())
Here are the lines of Python code required to add a check box in the resulting GUI:
We are creating three Checkbutton widgets that differ in their states.
The first is disabled and has a checkmark in it. The user cannot remove this checkmark as the widget is
disabled.
The second Checkbutton is enabled and, by default, has no checkmark in it, but the user can click it to
add a checkmark.
The third Checkbutton is both enabled and checked by default. The users can uncheck and recheck the
widget as often as they like.
# Creating three checkbuttons #1
chVarDis = tk.IntVar() #2
check1=tk.Checkbutton(win, text="Disabled", variable=chVarDis, state ='disabled') # 3
check1.select() #4
check1.grid(column=0, row=4, sticky=tk.W) #5
chVarUn = tk.IntVar() #6
check2 = tk.Checkbutton(win, text="UnChecked", variable=chVarUn)
check2.deselect() #8
check2.grid(column=1, row=4, sticky=tk.W) #9
chVarEn = tk.IntVar() # 10
check3 = tk.Checkbutton(win, text="Enabled", variable=chVarEn)
check3.select() # 12
check3.grid(column=2, row=4, sticky=tk.W) # 13
In lines 2, 6, and 10, we create three variables of type IntVar. In the following line, for each of these
variables we create a Checkbutton, passing in these variables. They will hold the state of the
Checkbutton (unchecked or checked). By default, that is either 0 (unchecked) or 1 (checked) so the
type of the variable is a tkinter integer.
Setting the sticky property of the grid to tk.W means that the widget will be aligned to the west of the
grid. This is very similar to Java syntax and it means that it will be aligned to the left. When we resize
our GUI, the widget will remain on the left side and not be moved towards the center of the GUI.
Lines 4 and 12 place a checkmark into the Checkbutton widget by calling the select()method on these
two Checkbutton class instances.
Here are the lines of Python code required to add a radio button in the resulting GUI:
# Radiobutton Globals #1
COLOR1 = "Blue" #2
COLOR2 = "Gold" #3
COLOR3 = "Red" #4
# Radiobutton Callback #5
def radCall(): #6
radSel=radVar.get()
if radSel == 1: win.configure(background=COLOR1)
elif radSel == 2: win.configure(background=COLOR2)
elif radSel == 3: win.configure(background=COLOR3)
# create three Radiobuttons #7
radVar = tk.IntVar() #8
rad1 = tk.Radiobutton(win, text=COLOR1, variable=radVar, value=1,command=radCall) # 9
rad1.grid(column=0, row=5, sticky=tk.W) # 10
rad2=tk.Radiobutton(win, text=COLOR2, variable=radVar, value=2, command=radCall) # 11
rad2.grid(column=1, row=5, sticky=tk.W) # 12
rad3=tk.Radiobutton(win, text=COLOR3, variable=radVar, value=3, command=radCall) # 13
rad3.grid(column=2, row=5, sticky=tk.W) # 14
In lines 2-4 we create some module-level global variables, which we will use in the creation of each
radio button as well as in the callback function that creates the action of changing the background
color of the main form (using the instance variable win).
In line 8 we are creating a tk.IntVar variable. What is important about this is that we are creating only
one variable to be used by all three radio buttons. We can check after execution, no matter which
Radiobutton we select, all the others will automatically be unselected for us.
Here are the lines of Python code required to add a Text area in the resulting GUI:
ScrolledText widgets are much larger than simple Entry widgets and span multiple lines. They are
widgets like Notepad and wrap lines, automatically enabling vertical scrollbars when the text gets
larger than the height of the ScrolledText widget.
# Add this import to the top of the Python Module #1
from tkinter import scrolledtext #2
# Using a scrolled Text control #3
scrolW = 30 #4
scrolH = 3 #5
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD) #6
scr.grid(column=0, columnspan=3) #7
We can actually type into our widget, and if we type enough words, the lines will automatically wrap
around!
Once we type in more words than the height of the widget can display, the vertical scrollbar becomes
enabled. This all works out-of-the-box without us needing to write any more code to achieve this.
By setting the wrap property to tk.WORD we are telling the ScrolledText widget to break lines by
words, so that we do not wrap around within a word. The default option is tk.CHAR, which wraps any
character regardless of whether we are in the middle of a word.
Setting the columnspan property of the grid widget to 3 for the SrolledText widget makes this widget
span all three columns. If we did not set this property, our SrolledText widget would only reside in
column one, which is not what we want.
Layout Management
Arranging several labels within a label frame (Labelled
Panel) widget
The LabelFrame widget allows us to design our GUI in an organized fashion. We are still using the
grid layout manager as our main layout design tool, yet by using LabelFrame widgets we get much
more control over our GUI design.
Add the following code just above the main event loop towards the bottom of the Python module:
# Create a container to hold labels
labelsFrame = ttk.LabelFrame(win, text=' Labels in a Frame ') #1
labelsFrame.grid(column=0, row=7)
# Place labels into the container element #2
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=1, row=0)
ttk.Label(labelsFrame, text="Label3").grid(column=2, row=0)
# Place cursor into name Entry
nameEntered.focus()
# Place labels into the container element – vertically #3
ttk.Label(labelsFrame, text="Label1").grid(column=0, row=0)
ttk.Label(labelsFrame, text="Label2").grid(column=0, row=1)
ttk.Label(labelsFrame, text="Label3").grid(column=0, row=2)
Comment # 1: Here, we will create our first ttk LabelFrame widget and give the frame a name. The
parent container is win, our main window.
The three lines following comment # 2 create label names and place them in the LabelFrame. We are
using the important grid layout tool to arrange the labels within the LabelFrame. The column and row
properties of this layout manager give us the power to control our GUI layout.
The highlighted comment # 3 shows how easy it is to change our layout via the column and row
properties. Note how we change the column to 0, and how we layer our labels vertically by numbering
the row values sequentially.
In this diagram, win is the variable that references our main GUI tkinter window frame; monty is the
variable that references our LabelFrame and is a child of the main window frame (win); and aLabel
and all other widgets are now placed into the LabelFrame container (monty).
Add the following code towards the top of our Python module (see comment # 1):
# Create instance
win = tk.Tk()
# Add a title
win.title("Python GUI")
# We are creating a container frame to hold all other widgets #1
monty = ttk.LabelFrame(win, text=' Monty Python ')
monty.grid(column=0, row=0)
Next, we will modify all the following controls to use monty as the parent, replacing win. Here is an
example of how to do this:
# Modify adding a Label
aLabel = ttk.Label(monty, text="A Label")
By assigning it "W" (West), we can control the widget to be left-aligned.
# Changing our Label
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')
The winfo_children() function returns a list of all the children belonging to the parent. This enables us
to loop through all of the widgets and change their properties.
Let's change the very long label back and align the entry in column 0 to the left.
ttk.Label(monty, text="Enter a name:").grid(column=0, row=0, sticky='W')
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky=tk.W)
By assigning the different available options to the relief property, we can create different appearances
for this widget.
Creating tooltips using Python
We are adding more useful functionality to our GUI. Surprisingly, adding a ToolTip to our controls
should be simple, but it is not as simple as we wish it to be. In order to achieve this desired
functionality, we will place our ToolTip code into its own OOP class.
Add this class just below the import statements:
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, _cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 27
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID,
borderwidth=1, font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
#===========================================================
def createToolTip( widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
In an object-oriented programming (OOP) approach we create a new class in our Python module.
Python allows us to place more than one class into the same Python module and it also enables us to
"mix-and-match" classes and regular functions in the same module.
The code above is doing exactly this.
The ToolTip class is a Python class and in order to use it, we have to instantiate it.
If you are not familiar with OOP programming, "instantiating an object to create an instance of the
class" may sound rather boring.
The principle is quite simple and very similar to creating a Python function via a def statement and
then later in the code actually calling this function.
In a very similar manner, we first create a blueprint of a class and simply assign it to a variable by
adding parentheses to the name of the class as follows:
class AClass():
pass
instanceOfAClass = AClass()
print(instanceOfAClass)
The above code prints out a memory address and also shows that our variable now has a reference to
this class instance.
The cool thing about OOP is that we can create many instances of the same class.
In our preceding code, we declare a Python class and explicitly make it inherit from the object that is
the foundation of all Python classes. We can also leave it out as we have done in the AClass code
example because it is the default for all Python classes.
After all of the necessary tooltip creation code that occurs within the ToolTip class, we next switch
over to non-OOP Python programming by creating a function just below it.
We define the function createToolTip() and it expects one of our GUI widgets to be passed in as an
argument so we can display a ToolTip when we hover our mouse over this control.
The createToolTip() function actually creates a new instance of our ToolTip class for every widget we
call it for.
We can add a tooltip for our Spinbox widget, like this:
# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')
As well as for all of our other GUI widgets in the very same manner. We just have to pass in the parent
of the widget we wish to have a tooltip displaying some extra information. For our ScrolledText
widget we made the variable scr point to it so this is what we pass into the constructor of our ToolTip
creation function.
# Using a scrolled Text control
scrolW = 30; scrolH = 3
scr = scrolledtext.ScrolledText(monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, row=3, sticky='WE', columnspan=3)
# Add a Tooltip to the ScrolledText widget
createToolTip(scr, 'This is a ScrolledText widget.')
Add the following code just below the creation of the spinner:
# Add a Tooltip
createToolTip(spin, 'This is a Spin control.')
Now, when we hover the mouse over the spinner widget, we get a tooltip, providing additional
information to the user.
First, we import the tkinter module and alias it to the name tk.
Next, we use this alias to create an instance of the Tk class by appending parentheses to Tk, which
calls the constructor of the class. This is the same mechanism as calling a function, only here we are
creating an instance of a class.
Usually we use this instance assigned to the variable win to start the main event loop later in the code.
But here, we are not displaying a GUI but demonstrating how to use the tkinter StringVar type.
Then we create an instance of the tkinter StringVar type and assign it to our Python strData variable.
After that, we use our variable to call the set() method on StringVar and, after having it set to a value,
we then get the value and save it in a new variable named varData and then print out its value.
In the Eclipse PyDev console, towards the bottom of the screenshot, we can see the output printed to
the console, which is Hello StringVar.
Next, we will print the default values of tkinter's IntVar, DoubleVar, and BooleanVar types.
As can be seen in the preceding screenshot, the default values do not get printed as we would have
expected.
The online literature mentions default values, but we won't see those values until we call the get
method on them. Otherwise, we just get a variable name that automatically increments (for example
PY_VAR3, as can be seen in the preceding screenshot).
Assigning the tkinter type to a Python variable does not change the outcome. We still do not get the
default value.
Here, we are focusing on the simplest code (which creates PY_VAR0):
The value is PY_VAR0, not the expected 0, until we call the get method. Now we can see the default
value. We did not call set, so we see the default value automatically assigned to each tkinter type once
we call the get method on each type.
Notice how the default value of 0 gets printed to the console for the IntVar instance we saved in the
intData variable. We can also see the values in the Eclipse PyDev debugger window at the top of the
screenshot.
How to get data from a widget
When the user enters data, we want to do something with it in our code. This recipe shows how to
capture data in a variable. In the previous recipe, we created several tkinter class variables. They were
standalone. Now we are connecting them to our GUI, using the data we get from the GUI and storing
it in Python variables.
We are assigning a value from our GUI to a Python variable. Add the following code towards the
bottom of our module, just above the main event loop:
strData = spin.get()
print("Spinbox value: " + strData)
# Place cursor into name Entry
nameEntered.focus()
#======================
# Start GUI
#======================
win.mainloop()
Running the code gives us the following result:
We are retrieving the current value of the Spinbox control.
We created our Spinbox widget using the following code, hard-coding the available values into
it:
# Adding a Spinbox widget using a set of values
spin = Spinbox(monty, values=(1, 2, 4, 42, 100), width=5, bd=8, command=_spin)
spin.grid(column=0, row=2)
We can also move the hard-coding of the data out of the creation of the Spinbox class instance and set
it later.
# Adding a Spinbox widget assigning values after creation
spin = Spinbox(monty, width=5, bd=8, command=_spin)
spin['values'] = (1, 2, 4, 42, 100)
spin.grid(column=0, row=2)
It does not matter how we create our widget and insert data into it because we can access this data by
using the get) method on the instance of the widget.
We define a global variable at the top of our module and, later, towards the bottom of our module, we
print out its value.
That works.
Add this function towards the bottom of our module:
Above, we are using the module-level global. It is easy to make a mistake by shadowing the global, as
demonstrated in the following screenshot:
Note how 42 became 777, even though we are using the same variable name.
Using the global qualifier (line 234) prints out the value we originally assigned it (42) towards the top
of our module, as can be seen in the following screenshot:
But, be careful. When we uncomment the local global, we print out the value of the local, not the
global:
Even though we are using the global qualifier, the local variable seems to override it. We are getting a
warning from the Eclipse PyDev plug-in that our GLOBAL_CONST = 777 is not being used, yet
running the code still prints 777 instead of the expected 42.
This might not be the behavior we expect. Using the global qualifier we might expect that we are
pointing to the global variable created earlier.
Instead, it seems that Python creates a new global variable in a local function and overwrites the one
we created earlier.
Global variables can be very useful when programming small applications. They can help to make data
available across methods and functions within the same Python module and sometimes the overhead of
OOP is not justified.
As our programs grow in complexity, the benefit we gained from using globals can quickly diminish.
We played around with global variables within procedural code and learned how that can lead to hard-
to-debug bugs. In the next chapter, we will move on to OOP, which can eliminate these kinds of bugs.
And the broken out (aka refactored) code in a separate module looks like this:
In the preceding screenshots, we can see several tooltip messages being displayed. The one for the
main window might appear a little bit annoying, so it is better not to display a tooltip for the main
window because we really wish to highlight the functionality of the individual widgets. The main
window form has a title that explains its purpose; no need for a tooltip.