9. GUIs with Tkinter#
This tutorial consists of minimal examples and explains common mistakes.
9.1. Which GUI toolkit?#
GUI is short for Graphical User Interface. It means a program that we can use without a command prompt or a terminal, like a web browser, a file manager or an editor.
Tkinter is an easy way to write GUIs in Python. Unlike bigger GUI toolkits like Qt and GTK+, tkinter comes with Python so many Python users have it already. Tkinter works on Windows, Mac OSX and Linux, so it’s a good choice for writing cross-platform programs.
Tkinter is light, but it’s also limited in some ways. For example, you can’t write a web browser in tkinter. Simpler things like text editors and music players can be written in tkinter.
9.2. List of contents#
[Getting Started]
[Buttons]
[Geometry Managers]
[Dialogs]
[Event Loop and Threads]
10. Getting Started#
Tkinter is a wrapper around a GUI toolkit called Tk. It’s usually used through a language called Tcl, but tkinter allows us to use Tcl and Tk through Python. This means that we can use Tcl’s libraries like Tk with Python without writing any Tcl code.
10.1. Installing tkinter#
If you installed Python yourself you already have tkinter.
10.2. Hello World!#
That’s enough talking. Let’s do the classic Hello World program :)
import tkinter
root = tkinter.Tk()
label = tkinter.Label(root, text="Hello World!")
label.pack()
root.mainloop()
Run this program like any other program. It should display a tiny window
with the text Hello World!
in it. I’m way too lazy to take
screenshots, so here’s some ASCII art:
,------------------.
| tk | _ | o | X |
|------------------|
| Hello World! |
`------------------'
There are many things going on in this short code. Let’s go through it line by line.
import tkinter
Many other tkinter tutorials use from tkinter import *
instead, and then they
use things like Label
instead of tk.Label
. Don’t use star imports. It
will confuse you and other people. You can’t be sure about where Label
comes
from if the file is many lines long, but many tools that process code
automatically will also be confused. Overall, star imports are evil.
Some people like to do import tkinter as tk
and then use tk.Label
instead
of tkinter.Label
. There’s nothing wrong with that, and you can do that too if
you want to. It’s also possible to do from tkinter import Tk, Label, Button
,
which feels a little bit like a star import to me, but it’s definitely not as
bad as a star import.
root = tkinter.Tk()
The root window is the main window of our program. In this case, it’s the only window that our program creates. Tkinter starts Tcl when you create the root window.
label = tkinter.Label(root, text="Hello World!")
Like most other GUI toolkits, Tk uses widgets. A widget is something that we see on the screen. Our program has two widgets. The root window is a widget, and this label is a widget. A label is a widget that just displays text.
Note that most widgets take a parent widget as the first argument.
When we do tk.Label(root)
, the root window becomes the parent so the
label will be displayed in the root window.
label.pack()
This adds the label into the root window so we can see it.
Instead of using a label
variable, you can also do this:
tkinter.Label(root, text="Hello World!").pack()
But don’t do this:
label = tkinter.Label(root, text="Hello World!").pack() # this is probably a mistake
Look carefully, the above code creates a label, packs it, and then sets a
label
variable to whatever the .pack()
returns. That is not same as the
label widget. It is None
because most functions return None
when they don’t
need to return anything more meaningful, so you’ll get errors like
NoneType object something something
when you try to use the label
.
root.mainloop()
The code before this takes usually just a fraction of a second to run, but this line of code runs until we close the window. It’s usually something between a few seconds and a few hours.
10.3. Hello Ttk!#
There is a Tcl library called Ttk, short for “themed tk”.
The tkinter.ttk
module contains things for using Ttk with Python. Here is a
tkinter program that displays two buttons that do nothing when they are
clicked. One of the buttons is a Ttk button and the other isn’t.
import tkinter
from tkinter import ttk
root = tkinter.Tk()
tkinter.Button(root, text="Click me").pack()
ttk.Button(root, text="Click me").pack()
root.mainloop()
The GUI looks like this:
The ttk button is the one that looks a lot better. Tk itself is very old, and its button widget is probably just as old, and it looks very old too. Ttk widgets look much better in general, and you should always use Ttk widgets. tl;dr: Use ttk.
If you already have some code that doesn’t use Ttk widgets, you just need to
add from tkinter import ttk
and then replace tkinter.SomeWidget
with
ttk.SomeWidget
everywhere. Most things will work just fine.
Unfortunately writing Ttk code is kind of inconvenient because there’s no way to create a root window that uses Ttk. Some people just create a Tk root window and add things to that, e.g. like this:
import tkinter
from tkinter import ttk
root = tkinter.Tk()
ttk.Label(root, text="Hello!").pack()
ttk.Button(root, text="Click me").pack()
root.mainloop()
The background of this root window is not using Ttk, so it has very different
colors than the Ttk widgets. We can fix that by adding a big Ttk frame to the
Tk root window. A frame is an empty widget, and we can add any other widgets we
want inside a frame. If we add a ttk.Frame
to the root window and pack it
with fill='both', expand=True
, it will always fill the entire window, making
the window look like a Ttk widget. We’ll do this in all examples from now on.
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
# now add all widgets to big_frame instead of root
This is annoying, but keep in mind that you only need to do this once for each window; in a big project you typically have many widgets inside each window, and this extra boilerplate doesn’t annoy that much after all.
Here is a complete hello world program.
import tkinter
from tkinter import ttk
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame, text="Hello World!")
label.pack()
root.mainloop()
10.4. Tkinter and the >>>
prompt#
The >>>
prompt is a great way to experiment with things. You can also
experiment with tkinter on the >>>
prompt, but unfortunately it
doesn’t work that well on all platforms. Everything works great on
Linux, but on Windows the root window is unresponsive when
root.mainloop()
is not running.
Try the hello world example on the >>>
prompt, just type it there line
by line. It’s really cool to see the widgets appearing on the screen as
you type. If this works on your system, that’s great! If not,
you can run root.update()
regularly to make it display your changes.
10.5. Widget options#
When we created a label like label = tk.Label(root, text="Hello World!")
,
we got a label that displayed the text “Hello World!”. Here text
was
an option, and its value was "Hello World!"
.
We can also change the text after creating the label in a few different ways:
>>> label['text'] = "New text" # it behaves like a dict
>>> label.config(text="New text") # this does the same thing
>>> label.configure(text="New text") # this does the same thing too
The config
and configure
methods do the same thing, only their names
are different.
It’s also possible to get the text after setting it:
>>> label['text'] # again, it behaves like a dict
'New text'
>>> label.cget('text') # or you can use a method instead
'New text'
There are multiple different ways to do the same things, and you can mix them however you want.
You can also convert the label to a dict to see all of the options and their values:
>>> dict(label)
{'padx': <pixel object: '1'>, 'pady': <pixel object: '1'>, 'borderwidth': <pixe
l object: '1'>, 'cursor': '', 'state': 'normal', 'image': '', 'bd': <pixel obje
ct: '1'>, 'font': 'TkDefaultFont', 'justify': 'center', 'height': 0, 'highlight
color': '#ffffff', 'wraplength': <pixel object: '0'>, 'activebackground': '#ece
cec', 'foreground': '#ffffff', 'anchor': 'center', 'bitmap': '', 'fg': '#ffffff
', 'compound': 'none', 'textvariable': '', 'underline': -1, 'background': '#3b3
b3e', 'width': 0, 'highlightbackground': '#3b3b3e', 'relief': 'flat', 'bg': '#3
b3b3e', 'text': '', 'highlightthickness': <pixel object: '0'>, 'takefocus': '0'
, 'disabledforeground': '#a3a3a3', 'activeforeground': '#000000'}
If we use pprint.pprint:
>>> import pprint
>>> pprint.pprint(dict(label))
{'activebackground': '#ececec',
'activeforeground': '#000000',
'anchor': 'center',
'background': '#3b3b3e',
...
10.6. Manual pages#
Tkinter is not documented very well, but there are many good manual pages about Tk written for Tcl users. Tcl and Python are two different languages, but Tk’s manual pages are easy to apply to tkinter code. You’ll find the manual pages useful later in this tutorial.
This tutorial contains links to the manual pages, like this ttk_label(3tk) link. There’s also a list of the manual pages.
10.7. Summary#
Tkinter is an easy way to write cross-platform GUIs.
Now you should have tkinter installed and you should know how to use it on the
>>>
prompt.Don’t use star imports.
Now you should know what a widget is.
Use ttk widgets with
from tkinter import ttk
, and always create a bigttk.Frame
packed withfill='both', expand=True
into each root window.You can set the values of Tk’s options when creating widgets like
label = tk.Label(root, text="hello")
, and you can change them later using any of these ways:label['text'] = "new text" label.config(text="new text") label.configure(text="new text")
You can get the current values of options like this:
print(label['text']) print(label.cget('text'))
If
print(something)
prints something weird, tryprint(repr(something))
.You can view all options and their values like
pprint.pprint(dict(some_widget))
. The options are explained in the manual pages.
11. Buttons#
In this section we can focus on doing fun things with tkinter.
11.2. Blocking callback functions#
In tkinter and other GUI toolkits, all callbacks should run about 0.1 seconds or less. Let’s make a callback function that runs for 5 seconds and see what happens:
import time
import tkinter
from tkinter import ttk
def ok_callback():
print("hello")
def stupid_callback():
time.sleep(5)
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button1 = ttk.Button(big_frame, text="This is OK", command=ok_callback)
button1.pack()
button2 = ttk.Button(big_frame, text="This sucks", command=stupid_callback)
button2.pack()
root.mainloop()
Now run the program and click the “this sucks” button. It stays pressed down for 5 seconds. But try to do something else while it’s pressed down. You can’t click the “this is OK” button and you can’t even close the window! This program sucks.
The problem is that tkinter and other GUI toolkits do only one thing at
a time. Tkinter can’t run our ok_callback
while it’s running
stupid_callback
, and it can’t even close the root window. This isn’t
limited to button commands, all tkinter callbacks should take at most
0.1 seconds.
Doing multiple things at the same time is an advanced topic, and we’ll learn more about it later.
11.3. Passing arguments to callback functions#
Let’s say that we want to make a program with 5 buttons that print “hello 1”, “hello 2” and so on. Does it mean that we need to define 5 functions?
def print_hello_1():
print("hello 1")
def print_hello_2():
print("hello 2")
...
There’s a better way, and it’s called functools.partial. It works like this:
>>> import functools
>>> thing = functools.partial(print, "hello")
>>> thing() # runs print("hello")
hello
>>> thing("world") # runs print("hello", "world")
hello world
>>> thing(1, 2, 3) # runs print("hello", 1, 2, 3)
hello 1 2 3
So we can write code like this:
import functools
import tkinter
from tkinter import ttk
def print_hello_number(number):
print("hello", number)
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
for i in range(1, 6):
button = ttk.Button(big_frame, text="Hello %d" % i,
command=functools.partial(print_hello_number, i))
button.pack()
root.mainloop()
Some people would use a lambda function instead, but using lambda functions in loops should be use with care.
11.4. Summary#
The
ttk.Button
widget displays a button.Buttons have a
command
option. It can be set to a function that runs when the button is clicked.Button commands and other callbacks should not block. It means that they should run only a short time, about 0.1 seconds or less.
Use
functools.partial
when you need to pass arguments to callbacks.
12. Geometry Managers#
So far we have used the pack()
method to add labels and buttons to our
root window. Pack is one of the simplest geometry managers in Tk. In
this section we’ll learn more about pack and other geometry managers.
12.1. Pack#
The pack(3tk) geometry manager is really simple and easy to use. Let’s create a window like this with it:
,--------------------------------------------.
| Pack Test | _ | o | X |
|--------------------------------------------|
|,--------------------. |
|| | |
|| | |
|| | |
|| This stretches | This doesn't stretch |
|| | |
|| | |
|| | |
|`--------------------' |
|--------------------------------------------|
| This is a status bar |
`--------------------------------------------'
Here’s the code.
import tkinter
from tkinter import ttk
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button = ttk.Button(big_frame, text="This stretches")
label = ttk.Label(big_frame, text="This doesn't stretch")
statusbar = ttk.Label(big_frame, text="This is a status bar", relief='sunken')
statusbar.pack(side='bottom', fill='x')
button.pack(side='left', fill='both', expand=True)
label.pack(side='left')
root.title("Pack Test")
root.geometry('300x150')
root.mainloop()
Run the program and change the size of the window by dragging its edges with the mouse. The button and status bar should stretch and shrink nicely.
Let’s go through the pack stuff in the code:
statusbar.pack(side='bottom', fill='x')
The fill='x'
means that the status bar will fill all of the space it
has horizontally. We can also do fill='y'
or fill='both'
.
Obviously, side='bottom'
means that the status bar will appear at the
bottom of the window. The default is side='top'
. Note that I packed
the status bar before packing any other widgets, because even with
side='bottom'
, it won’t go below widgets that are already packed with
side='left'
or side='right'
.
button.pack(side='left', fill='both', expand=True)
We want the button to fill all the space it has, so we’re using
fill='both'
. But we also want to give it as much space as possible, so
we’re using expand=True
. If you remove the expand=True
and run the
program again, you’ll notice that the button doesn’t stretch anymore
becuase it doesn’t have any space to fill.
label.pack(side='left')
We packed the button with side='left'
first and then the label, so
they ended up next to each other so that the button is on the left side
of the label.
12.2. Grid#
Let’s make a similar GUI as in the pack example, but with grid(3tk) instead of pack.
import tkinter
from tkinter import ttk
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button = ttk.Button(big_frame, text="This stretches")
label = ttk.Label(big_frame, text="This doesn't stretch")
statusbar = ttk.Label(big_frame, text="This is a status bar", relief='sunken')
button.grid(row=0, column=0, sticky='nswe')
label.grid(row=0, column=1)
statusbar.grid(row=1, column=0, columnspan=2, sticky='we')
big_frame.grid_rowconfigure(0, weight=1)
big_frame.grid_columnconfigure(0, weight=1)
root.title("Grid Test")
root.geometry('300x150')
root.mainloop()
The code is mostly the same. Let’s go through the differences:
button.grid(row=0, column=0, sticky='nswe')
The row=0
and column=0
mean that the button will end up in the
top left corner of our root window. In math, the coordinate (0, 0)
usually means the bottom left corner, but in programming it almost
always means the top left corner.
sticky='nswe'
might look weird at first, but it’s actually simple to
understand. The n means north, s means south and so on.
The sticky option is kind of like the fill option of pack()
.
sticky='nswe'
is like fill='both'
, sticky='we'
is like fill='x'
and so on.
label.grid(row=0, column=1)
This adds the label next to the button. It doesn’t stretch because we
didn’t set a sticky value, and it defaults to ''
.
statusbar.grid(row=1, column=0, columnspan=2, sticky='we')
columnspan=2
means that the status bar will be two columns wide, so it
fills the entire width of the window because we have two columns.
Note that we don’t have to grid the statusbar before gridding other widgets. Each widget goes to its own row and column so it doesn’t matter which order we add them in.
big_frame.grid_rowconfigure(0, weight=1)
big_frame.grid_columnconfigure(0, weight=1)
This is like setting expand=True
with pack. The button is packed to
row 0 and column 0, and this gives it as much space as possible.
Here’s another, more advanced grid example:
,-------------------.
| Calculator | X |
|-------------------|
| 7 | 8 | 9 | * | / |
|---+---+---+---+---|
| 4 | 5 | 6 | + | - |
|---+---+---+-------|
| 1 | 2 | 3 | |
|-------+---| = |
| 0 | . | |
`-------------------'
Something like this would be almost impossible to do with pack, but it’s easy with grid:
import tkinter
from tkinter import ttk
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
# None means that we'll create the button later
rows = [
['7', '8', '9', '*', '/'],
['4', '5', '6', '+', '-'],
['1', '2', '3', None, None],
[None, None, '.', None, None],
]
for y, row in enumerate(rows):
for x, character in enumerate(row):
if character is not None:
# try this without width=3 so you'll know why i put it there
button = ttk.Button(big_frame, text=character, width=3)
button.grid(row=y, column=x, sticky='nswe')
# the widths of these buttons are set to smallest possible values because grid
# will make sure that they are wide enough, e.g. zerobutton is below '1' and
# '2', and it will have the same width as the '1' and '2' buttons together
zerobutton = ttk.Button(big_frame, text='0', width=1)
zerobutton.grid(row=3, column=0, columnspan=2, sticky='nswe')
equalbutton = ttk.Button(big_frame, text='=', width=1)
equalbutton.grid(row=2, column=3, rowspan=2, columnspan=2, sticky='nswe')
# let's make everything stretch when the window is resized
for x in range(5):
big_frame.grid_columnconfigure(x, weight=1)
for y in range(4):
big_frame.grid_rowconfigure(y, weight=1)
root.title("Calculator")
root.mainloop()
12.3. Place#
The place(3tk) geometry manager can be used for absolute positioning with pixels, and that’s almost always a bad idea. But place supports relative positioning too, and it’s useful for things like message boxes. Tk has built-in message boxes too, but place is useful if you want a customized message box.
,-----------------------------------.
| Important Message | _ | o | X |
|-----------------------------------|
| |
| |
| This is a very important message. |
| |
| |
| |
| |
| ,------. |
| | OK | |
| `------' |
`-----------------------------------'
Here’s the code:
import tkinter
from tkinter import ttk
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame, text="This is a very important message.")
label.place(relx=0.5, rely=0.3, anchor='center')
button = ttk.Button(big_frame, text="OK", command=root.destroy)
button.place(relx=0.5, rely=0.8, anchor='center')
root.title("Important Message")
root.geometry('250x150')
root.mainloop()
Run the program and resize the window. The label and the button should “float” in it nicely.
label.place(relx=0.5, rely=0.3, anchor='center')
The relx
and rely
options are actually short for “relative x” and
“relative y”, not relaxing and relying. This means that the center of
the button will always be 50% from the left side of the window and 30%
from the top of the window. Other valid anchor values are n like north,
nw like north-west and so on.
button = ttk.Button(..., command=root.destroy)
This example isn’t as boring as our calculator is because the OK button
actually works. Calling root.destroy()
stops root.mainloop()
.
12.4. Which geometry manager should I use?#
Pack is good for laying out big things. If you have a program with a few big widgets next to each other and a statusbar, pack is the best way to add them to the root window.
Grid works for big things too, but it’s best for things that are obviously grids, like the calculator.
Place is the easiest choice when you want to use percents to position widgets relatively, like we did in the example. This is useful with things like message dialogs.
12.5. Combining the geometry managers#
Only use one geometry manager in one widget. The results can be surprising if you first grid something and then pack something else. Unfortunately tkinter doesn’t raise an exception if you try to do that, but don’t do it.
You can still use multiple geometry managers in one program with
ttk.Frame
. The frame is a simple widget that can be added to any other
parent widget, and then other widgets can be added into the frame with a
different geometry manager.
For example, this program uses pack, grid and place, but in separate frames:
import tkinter
from tkinter import ttk
def make_calculator_frame(big_frame):
frame = ttk.Frame(big_frame)
rows = [
['7', '8', '9', '*', '/'],
['4', '5', '6', '+', '-'],
['1', '2', '3', None, None],
[None, None, '.', None, None],
]
for y, row in enumerate(rows):
for x, character in enumerate(row):
if character is not None:
button = ttk.Button(frame, text=character, width=3)
button.grid(row=y, column=x, sticky='nswe')
zerobutton = ttk.Button(frame, text='0', width=1)
zerobutton.grid(row=3, column=0, columnspan=2, sticky='nswe')
equalbutton = ttk.Button(frame, text='=', width=1)
equalbutton.grid(row=2, column=3, rowspan=2, columnspan=2, sticky='nswe')
for x in range(5):
frame.grid_columnconfigure(x, weight=1)
for y in range(4):
frame.grid_rowconfigure(y, weight=1)
return frame
def make_message_frame(big_frame):
frame = ttk.Frame(big_frame)
label = ttk.Label(frame, text="This is a very important message.")
label.place(relx=0.5, rely=0.3, anchor='center')
button = ttk.Button(frame, text="OK")
button.place(relx=0.5, rely=0.8, anchor='center')
return frame
def main():
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
calculator = make_calculator_frame(big_frame)
message = make_message_frame(big_frame)
statusbar = ttk.Label(big_frame, text="This is a status bar.",
relief='sunken')
statusbar.pack(side='bottom', fill='x')
calculator.pack(side='left', fill='y')
message.pack(side='left', fill='both', expand=True)
root.title("Useless GUI")
root.geometry('450x200')
root.minsize(400, 100)
root.mainloop()
if __name__ == '__main__':
main()
12.6. Summary#
Geometry managers are used for adding child widgets to parent widgets.
Use pack for big and simple layouts, grid for griddy things and place for relative things.
Don’t use multiple geometry managers in one widget. You can mix different geometry managers with
ttk.Frame
by using one geometry manager in each frame.
13. Message Dialogs#
Now we know how to make a button that prints a message on the terminal, we want to display a nice message box instead.
13.1. Built-in dialogs#
Tkinter comes with many useful functions for making message dialogs. Here’s a simple example:
import tkinter
from tkinter import ttk, messagebox
def do_hello_world():
messagebox.showinfo("Important Message", "Hello World!")
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button = ttk.Button(big_frame, text="Click me", command=do_hello_world)
button.pack()
root.mainloop()
That’s pretty cool. We can create a dialog with a label and an OK button and wait for the user to click the button with just one function call. [The callback is blocking], but it doesn’t matter here because Tk handles dialogs specially.
The tkinter.messagebox
module supports many other things too, and
there are other modules like it as well. Here’s an example that
demonstrates most of the things that are possible. You can use this
program when you need to use a dialog so you don’t need to remember
which function to use.
Note that many of these functions return None if you close the dialog using the X button in the corner.
import functools
import tkinter
from tkinter import ttk, messagebox, filedialog, simpledialog, colorchooser
class Demo:
def __init__(self, big_frame, modulename):
self.frame = ttk.LabelFrame(big_frame, text=("tkinter." + modulename))
self.modulename = modulename
# this makes buttons that demonstrate messagebox functions
# it's a bit weird but it makes this code much less repetitive
def add_button(self, functionname, function, args=(), kwargs=None):
# see http://stackoverflow.com/q/1132941
if kwargs is None:
kwargs = {}
# the call_string will be like "messagebox.showinfo('Bla Bla', 'Bla')"
parts = []
for arg in args:
parts.append(repr(arg))
for key, value in kwargs.items():
parts.append(key + "=" + repr(value))
call_string = "%s.%s(%s)" % (self.modulename, functionname,
', '.join(parts))
callback = functools.partial(self.on_click, call_string,
function, args, kwargs)
button = ttk.Button(self.frame, text=functionname, command=callback)
button.pack()
def on_click(self, call_string, function, args, kwargs):
print('running', call_string)
result = function(*args, **kwargs)
print(' it returned', repr(result))
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
msgboxdemo = Demo(big_frame, "messagebox")
msgboxdemo.add_button(
"showinfo", messagebox.showinfo,
["Important Message", "Hello World!"])
msgboxdemo.add_button(
"showwarning", messagebox.showwarning,
["Warny Warning", "This may cause more problems."])
msgboxdemo.add_button(
"showerror", messagebox.showerror,
["Fatal Error", "Something went wrong :("])
msgboxdemo.add_button(
"askyesno", messagebox.askyesno,
["Important Question", "Do you like this?"])
msgboxdemo.add_button(
"askyesnocancel", messagebox.askyesnocancel,
["Important Question", "Do you like this?"])
msgboxdemo.add_button(
"askokcancel", messagebox.askokcancel,
["Stupid Question", "Do you really want to do this?"])
msgboxdemo.add_button(
"askyesnocancel", messagebox.askyesnocancel,
["Save Changes?", "Do you want to save your changes before quitting?"])
filedialogdemo = Demo(big_frame, "filedialog")
filedialogdemo.add_button(
"askopenfilename", filedialog.askopenfilename,
kwargs={'title': "Open File"})
filedialogdemo.add_button(
"asksaveasfilename", filedialog.asksaveasfilename,
kwargs={'title': "Save As"})
simpledialogdemo = Demo(big_frame, "simpledialog")
simpledialogdemo.add_button(
"askfloat", simpledialog.askfloat,
["Pi Question", "What's the value of pi?"])
simpledialogdemo.add_button(
"askinteger", simpledialog.askinteger,
["Computer Question", "How many computers do you have?"])
simpledialogdemo.add_button(
"askstring", simpledialog.askstring,
["Editor Question", "What is your favorite editor?"])
colorchooserdemo = Demo(big_frame, "colorchooser")
colorchooserdemo.add_button(
"askcolor", colorchooser.askcolor,
kwargs={'title': "Choose a Color"})
msgboxdemo.frame.grid(row=0, column=0, rowspan=3)
filedialogdemo.frame.grid(row=0, column=1)
simpledialogdemo.frame.grid(row=1, column=1)
colorchooserdemo.frame.grid(row=2, column=1)
root.title("Dialog Tester")
root.resizable(False, False)
root.mainloop()
As you can see, using a
class
is handy with bigger programs. I explained functools.partial
before, so you can
read that if you haven’t seen it before.
Many of these functions can take other keyword arguments too. Most of them are explained in [the manual pages].
Python module |
Manual page |
---|---|
|
|
|
|
|
|
|
No manual page, but the module seems to be from this tutorial. |
13.2. Message dialogs without the root window#
Let’s try displaying a message without making a root window first and see if it works:
>>> from tkinter import messagebox
>>> messagebox.askyesno("Test", "Does this work?")
This code creates a message box correctly, but it also creates a stupid window in the background. The problem is that tkinter needs a root window to display the message, but we didn’t create a root window yet so tkinter created it for us.
Sometimes it makes sense to display a message dialog without creating any other windows. For example, our program might need to display an error message and exit before the main window is created.
In these cases, we can create a root window and hide it with the
withdraw()
method. It’s documented in wm(3tk)
as wm withdraw
; you can scroll down to it or just press Ctrl+F
and type “withdraw”.
import tkinter
from tkinter import messagebox
root = tkinter.Tk()
root.withdraw()
messagebox.showerror("Fatal Error", "Something went badly wrong :(")
The root window hides itself so quickly that we don’t notice it at all. It’s
also possible to unhide it like root.deiconify()
.
13.3. Custom dialogs#
Tkinter’s default dialogs are enough most of the time, but sometimes it makes sense to create a custom dialog. For example, a tkinter program might have a custom setting dialog, but everything else would be done with tkinter’s dialogs.
It might be tempting to create multiple root windows, but don’t do this, this example is BAD:
import tkinter
from tkinter import ttk
def display_dialog():
root2 = tkinter.Tk() # BAD! NO!!
big_frame2 = ttk.Frame(root2)
big_frame2.pack(fill='both', expand=True)
ttk.Label(big_frame2, text="Hello World").place(relx=0.5, rely=0.3, anchor='center')
root2.mainloop()
root = tkinter.Tk()
button = ttk.Button(root, text="Click me", command=display_dialog)
button.pack()
root.mainloop()
Things like message boxes need a root window, and if we create more than one root window, we can’t be sure about which root window is used and we may get weird problems. Or worse, your code may work just fine for you and someone else running it will get weird problems.
The toplevel(3tk) widget is a window that uses an existing root window:
import tkinter
from tkinter import ttk
def display_dialog():
dialog = tkinter.Toplevel()
big_frame = ttk.Frame(dialog)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame, text="Hello World")
label.place(relx=0.5, rely=0.3, anchor='center')
dialog.transient(root)
dialog.geometry('300x150')
dialog.wait_window()
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button = ttk.Button(big_frame, text="Click me", command=display_dialog)
button.pack()
root.mainloop()
You probably understand how most of this code works, but there are a couple lines that need explanations:
dialog = tk.Toplevel()
The toplevel widget uses our root window even though we don’t explicitly
give it a root window. You can also do tk.Toplevel(root)
if you like
that more.
dialog.transient(root)
This line makes the dialog look like it belongs to the root window. It’s always in front of the root window, and it may be centered over the root window too. You can leave this out if you don’t like it.
dialog.wait_window()
This is like root.mainloop()
, it waits until the dialog is destroyed.
You don’t need to call this method if you want to let the user do other
things while the dialog is showing.
Clicking on the X button destroys the dialog, but it’s also possible to
destroy it like dialog.destroy()
. This is useful for creating buttons
that close the dialog:
okbutton = ttk.Button(dialog, text="OK", command=dialog.destroy)
13.4. “Do you want to quit” dialogs#
Many programs display a “do you want to save your changes?” dialog when
the user closes the main window. We can also do this with tkinter using
the root.protocol()
method. It’s documented in wm(3tk) as wm protocol
.
import tkinter
from tkinter import messagebox
def wanna_quit():
if messagebox.askyesno("Quit", "Do you really want to quit?"):
# the user clicked yes, let's close the window
root.destroy()
root = tkinter.Tk()
root.protocol('WM_DELETE_WINDOW', wanna_quit)
root.mainloop()
13.5. Running dialogs on startup#
Some programs display dialogs before the main window appears. We can also do that in tkinter.
import tkinter
from tkinter import ttk
# only use this before creating the main root window! otherwise you get
# two root windows at the same time, and that's bad, see above
def startup_dialog(labeltext):
result = None
rooty_dialog = tkinter.Tk()
big_frame = ttk.Frame(rooty_dialog)
big_frame.pack(fill='both', expand=True)
# result is left to None if the dialog is closed without clicking OK
def on_ok():
# usually 'result = entry.get()' creates a local variable called
# result, but this thing makes it also appear outside on_ok()
nonlocal result
result = entry.get()
rooty_dialog.destroy() # stops rooty_dialog.mainloop()
label = ttk.Label(big_frame, text=labeltext)
label.place(relx=0.5, rely=0.3, anchor='center')
entry = ttk.Entry(big_frame)
entry.place(relx=0.5, rely=0.5, anchor='center')
okbutton = ttk.Button(big_frame, text="OK", command=on_ok)
okbutton.place(relx=0.5, rely=0.8, anchor='center')
rooty_dialog.geometry('250x150')
rooty_dialog.mainloop()
# now the dialog's mainloop has stopped, so the dialog doesn't exist
# anymore and creating another root window is ok
return result
name = startup_dialog("Enter your name:")
if name is not None: # the user clicked OK
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame, text=("Hello %s!" % name))
label.pack()
root.mainloop()
Earlier I said that having multiple root windows at the same time is bad, but this is ok because we only have one root window at a time; we destroy the old root window before creating a new one.
13.6. Summary#
Tkinter comes with many handy dialogs functions. You can use the test program in this tutorial to decide which function to use.
If you don’t want a root window, you can create it and hide it immediately with the
withdraw()
method.Don’t use multiple root windows at the same time. Use
tk.Toplevel
instead; it has a handytransient()
method too.You can use
some_window.protocol('WM_DELETE_WINDOW', callback)
to change what clicking the X button does. You can close the window with thedestroy()
method.
14. Event Loop and Threads#
In the button chapter [we used time.sleep in a callback
function], and it froze
everything. This chapter is all about what happened and why. You’ll also
learn to use things like time.sleep
properly with your tkinter programs.
14.1. Tk’s Main Loop#
Before we dive into other stuff we need to understand what our
root.mainloop()
calls at the ends of our programs are doing.
When a tkinter program is running, Tk needs to process different kinds of events. For example, clicking on a button generates an event, and the main loop must make the button look like it’s pressed down and run our callback. Tk and most other GUI toolkits do that by simply checking for any new events over and over again, many times every second. This is called an event loop or main loop.
Button callbacks are also ran in the main loop. So if our button
callback takes 5 seconds to run, the main loop can’t process other events
while it’s running. For example, it can’t close the root window when we
try to close it. That’s why everything froze with our time.sleep(5)
callback.
14.2. After Callbacks#
The after
method is documented in after(3tcl), and it’s
an easy way to run stuff in Tk’s main loop. All widgets have this
method, and it doesn’t matter which widget’s after
method you use.
any_widget.after(milliseconds, callback)
runs callback()
after
waiting for the given number of milliseconds. The callback runs in Tk’s
mainloop, so it must not take a long time to run.
For example, this program displays a simple clock with after callbacks and time.asctime:
import time
import tkinter
from tkinter import ttk
# this must return soon after starting this
def change_text():
label['text'] = time.asctime()
# now we need to run this again after one second, there's no better
# way to do this than timeout here
root.after(1000, change_text)
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame, text='0')
label.pack()
change_text() # don't forget to actually start it :)
root.geometry('200x200')
root.mainloop()
14.3. Basic Thread Stuff#
So far we have avoided using functions that take a long time to complete in tkinter programs, but now we’ll do that with the threading module. Here’s a minimal example:
import threading
import time
import tkinter
from tkinter import ttk
# in a real program it's best to use after callbacks instead of
# sleeping in a thread, this is just an example
def blocking_function():
print("blocking function starts")
time.sleep(1)
print("blocking function ends")
def start_new_thread():
thread = threading.Thread(target=blocking_function)
thread.start()
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
button = ttk.Button(big_frame, text="Start the blocking function",
command=start_new_thread)
button.pack()
root.mainloop()
That’s pretty cool. The function runs for about a second, but it doesn’t freeze our GUI.
As usual, great power comes with great responsibility. Tkinter isn’t
thread-safe, so we must not do any tkinter stuff in threads. Don’t
do anything like label['text'] = 'hi'
or even print(label['text'])
.
It may kind of work for you, but it will make different kinds of weird
problems on some operating systems.
Think about it like this: in tkinter callbacks we can do stuff with tkinter and we need to return as soon as possible, but in threads we can do stuff that takes a long time to run but we must not touch tkinter. So we can use tkinter or run stuff that takes a long time, but not both in the same place.
It’s also possible to pass arguments to after callbacks:
# run print('hello') after 1 second
any_widget.after(1000, print, 'hello')
# run foo(bar, biz, baz) after 3 seconds
any_widget.after(3000, foo, bar, biz, baz)
Threads can handle arguments too, but they do it slightly differently:
# run foo(bar, biz, baz) in a thread
thread = threading.Thread(target=foo, args=[bar, biz, baz])
thread.start()
14.4. is_alive#
Thread objects have an is_alive()
method that returns True if the thread is
still running. It’s useful for doing stuff in tkinter when the thread has
finished. We’ll talk about moving more information from the thread back to
tkinter [later].
The only way to do something when is_alive()
returns False is to just check
is_alive()
repeatedly with after callbacks, kind of like how we updated our
clock repeatedly. Here’s an example:
import threading
import time
import tkinter
from tkinter import ttk, messagebox
def do_slow_stuff():
for i in range(1, 5):
print(i, '...')
time.sleep(1)
print('done!')
def check_if_ready(thread):
print('check')
if thread.is_alive():
# not ready yet, run the check again soon
root.after(200, check_if_ready, thread)
else:
messagebox.showinfo("Ready", "I'm ready!")
def start_doing_slow_stuff():
thread = threading.Thread(target=do_slow_stuff)
thread.start()
root.after(200, check_if_ready, thread)
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
ttk.Button(big_frame, text="Start", command=start_doing_slow_stuff).pack()
root.mainloop()
14.5. Moving stuff from threads to tkinter#
The thread world and tkinter’s mainloop world must be separated from each other, but we can move stuff between them with queues. They work like this:
>>> import queue
>>> the_queue = queue.Queue()
>>> the_queue.put("hello")
>>> the_queue.put("lol")
>>> the_queue.get()
'hello'
>>> the_queue.get()
'lol'
>>> the_queue.get() # this waits forever, press Ctrl+C to interrupt
Traceback (most recent call last):
...
KeyboardInterrupt
>>> the_queue.put('wololo')
>>> the_queue.get(block=False)
'wololo'
>>> the_queue.get(block=False)
Traceback (most recent call last):
...
queue.Empty
>>>
There are a few things worth noting:
If you make a variable called
queue
, then you can’t use thequeue
module. That’s why I named itthe_queue
instead.Things came out of the queue in the same order that we put them in. We put
"hello"
to the queue first, so we also got"hello"
out of it first. If someone talks about a FIFO queue or a First-In-First-Out queue, it means this.We are not using a list or collections.deque for this. Queues work better with threads.
If the queue is empty,
some_queue.get()
waits until we put something on the queue or we interrupt it. If we passblock=False
it raises aqueue.Empty
exception instead, and never waits for anything.
Usually I need queues for getting stuff from threads back to tkinter.
The thread puts something on the queue, and then an [after
callback] gets it from the queue with block=False
.
Like this:
import queue
import threading
import time
import tkinter
from tkinter import ttk
the_queue = queue.Queue()
def thread_target():
for number in range(10):
print("thread_target puts hello", number, "to the queue")
the_queue.put("hello {}".format(number))
time.sleep(1)
# let's tell after_callback that this completed
print('thread_target puts None to the queue')
the_queue.put(None)
def after_callback():
try:
message = the_queue.get(block=False)
except queue.Empty:
# let's try again later
root.after(100, after_callback)
return
print('after_callback got', message)
if message is not None:
# we're not done yet, let's do something with the message and
# come back later
label['text'] = message
root.after(100, after_callback)
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
label = ttk.Label(big_frame)
label.pack()
threading.Thread(target=thread_target).start()
root.after(100, after_callback)
root.geometry('200x200')
root.mainloop()
Checking if there’s something on the queue every 0.1 seconds may seem a bit weird, but unfortunately there’s no better way to do this. If checking the queue every 0.1 seconds is too slow for your program, you can use something like 50 milliseconds instead of 100.
Of course, you can use any other value you want instead of None. For
example, you could add STOP = object()
to the top of the program, and
then do things like if message is not STOP
.
14.6. Moving stuff from tkinter to threads#
We can also use queues to get things from tkinter to threads. Here we
put stuff to a queue in tkinter and wait for it in the thread, so we
don’t need block=False
. Here’s an example:
import queue
import threading
import time
import tkinter
from tkinter import ttk
the_queue = queue.Queue()
def thread_target():
while True:
message = the_queue.get()
print("thread_target: doing something with", message, "...")
time.sleep(1)
print("thread_target: ready for another message")
def on_click():
the_queue.put("hello")
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
ttk.Button(big_frame, text="Click me", command=on_click).pack()
threading.Thread(target=thread_target).start()
root.mainloop()
Run the program. You’ll notice that it kind of works, but the program just keeps running when we close the root window. You can interrupt it with Ctrl+C.
The problem is that Python is waiting for our thread to return, but it’s
running a while True
. To fix that, we need to modify our
thread_target
to stop when we put None on the queue, and then put a
None to the queue when root.mainloop
has completed. Like this:
import queue
import threading
import time
import tkinter
from tkinter import ttk
the_queue = queue.Queue()
def thread_target():
while True:
message = the_queue.get()
if message is None:
print("thread_target: got None, exiting...")
return
print("thread_target: doing something with", message, "...")
time.sleep(1)
print("thread_target: ready for another message")
def on_click():
the_queue.put("hello")
root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)
ttk.Button(big_frame, text="Click me", command=on_click).pack()
threading.Thread(target=thread_target).start()
root.mainloop()
# we get here when the user has closed the window, let's stop the thread
the_queue.put(None)
14.7. Summary#
Tk’s main loop checks for new events many times every second and does something when new events arrive.
If we tell the main loop to run something like
time.sleep(5)
it can’t do other things at the same time and everything freezes. Don’t do that.After callbacks tell the main loop to do something after some number of milliseconds. You can use them for running something repeatedly by calling the after method again in the callback.
If you need to run something that takes a long time, use threads. Don’t do tkinter stuff in threads, use queues for moving stuff between the mainloop and the thread instead.