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#

  1. [Getting Started]

  2. [Buttons]

  3. [Geometry Managers]

  4. [Dialogs]

  5. [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:

good-looking button and bad-looking button

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 big ttk.Frame packed with fill='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, try print(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.1. Our first button#

So far our programs just display text and that’s it. In this chapter we’ll add a button that we can click.

,---------------------------.
| Button Test   | _ | o | X |
|---------------------------|
|   This is a button test.  |
|      ,-------------.      |
|      |  Click me!  |      |
|      `-------------'      |
|                           |
|                           |
|                           |
`---------------------------'

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 button test.")
label.pack()
button = ttk.Button(big_frame, text="Click me!")
button.pack()

root.title("Button Test")
root.geometry('200x100')
root.minsize(150, 50)
root.mainloop()

As you can see, the code is mostly the same as in the hello world example, but there are some new things.

  • We can create ttk.Button widgets just like ttk.Label widgets.

  • We can pack() multiple widgets, and they end up below each other.

  • The title of our hello world window was “tk”, but it can be changed like root.title("new title").

  • I changed the size of the root window with root.geometry('200x100'). It was 200 pixels wide and 100 pixels high by default, but you can still resize the window by dragging its edges with the mouse. If you aren’t sure which default size you should use just try different sizes and see what looks best.

  • root.minsize(150, 50) means that the root window can’t be made smaller than 150 by 50 pixels. You can also do root.resizable(False, False) if you want to prevent the user from resizing the window at all.

Run the program. If you click the button, nothing happens at all.

As usual, all possible options are listed in the ttk_button(3tk) manual page. One of these options is command, and if we set it to a function it will be ran when the button is clicked. This program prints hello every time we click its button:

import tkinter
from tkinter import ttk


def print_hello():
    print("hello")


root = tkinter.Tk()
big_frame = ttk.Frame(root)
big_frame.pack(fill='both', expand=True)

button = ttk.Button(big_frame, text="Print hello", command=print_hello)
button.pack()
root.mainloop()

In this example, print_hello was a callback function. We don’t actually call it anywhere like print_hello(), but tkinter calls it when the button is clicked.

This program changes the text of a label whenever the button is clicked.

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')
label.pack()


def change_text():
    if label['text'] == 'Hello':
        label['text'] = 'World'
    else:
        label['text'] = 'Hello'


button = ttk.Button(big_frame, text="Click here", command=change_text)
button.pack()
root.mainloop()

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

tkinter.messagebox

tk_messageBox(3tk)

tkinter.filedialog

tk_getOpenFile(3tk) and tk_chooseDirectory(3tk)

tkinter.colorchooser

tk_chooseColor(3tk)

tkinter.simpledialog

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 handy transient() 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 the destroy() 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 the queue module. That’s why I named it the_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 pass block=False it raises a queue.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.