Python GUI Development with Tkinter: Part 3

This is the third installment of our multi-part series on developing GUIs in Python using Tkinter. Check out the links below for the other parts to this series:

Introduction

Tkinter is the de facto standard package for building GUIs in Python. In StackAbuse's first and second part of the Tkinter tutorial, we learned how to use the basic GUI building blocks to create simple interfaces.

In the last part of our tutorial, we'll take a look at a couple of shortcuts that Tkinter offers to let us effortlessly offer complex and very useful features. We'll also learn about Python Mega Widgets - a toolkit, based on Tkinter, that speeds up building complicated interfaces even faster.

File Dialog

Letting a user select a file on their machine is obviously a very common feature of graphical interfaces. The file dialogs are usually pretty complex - they combine at least multiple buttons (like Open, Cancel, or Create New Folder) and a frame that displays the structure of our environment's directories. Based on our previous tutorials, you may assume that using Tkinter it is very difficult to create such a complicated feature. However, actually, it isn't. Take a look at the following example:

import tkinter  
import tkinter.filedialog

root = tkinter.Tk()

def print_path():  
    f = tkinter.filedialog.askopenfilename(
        parent=root, initialdir='C:/Tutorial',
        title='Choose file',
        filetypes=[('png images', '.png'),
                   ('gif images', '.gif')]
        )

    print(f)

b1 = tkinter.Button(root, text='Print path', command=print_path)  
b1.pack(fill='x')

root.mainloop()  

Output:

The code above is all you need to display a nice, useful File Dialog. In line 2 we import the contents of the filedialog class. Then, after creating our root window in line 4, we define a new function in line 6 (which is supposed to be executed by the button created in line 17 and packed in line 18).

Let's take a look at the print_path() function definition. In line 7, we execute the askopenfilename function, which takes a couple of arguments. The first argument, of course, is the dialog's parent widget (which in this case is our root window). Then, in the initialdir argument, we provide a location that'll be displayed in our file dialog right after it's opened. title controls the content of the dialog's title bar.

And then we have the filetypes argument, thanks to which we can specify what kind of files will be visible for the user in the file dialog. Narrowing down the file types can make the search for the desired file much faster, as well as letting the user know which types of files are accepted.

The argument to filetypes is a list of 2-element tuples. In each tuple, the first element is a string which is any description we want to set for each of the file types. The second element is where we state or list the file extensions associated with each file type (if there's only one extension, it's a string - otherwise, it's a tuple). As you can see on the output screenshot above, the user can select the displayed file type from the dropdown list in the bottom right corner of the dialog.

The askopenfilename() method returns a string which is the path of the file selected by the user. If the user decides to hit Cancel, an empty string is returned. In line 7 we return the path to variable f, and then, in line 15 (which is only executed after the File Dialog is closed), the path is printed in the console.

Displaying Images Using Tkinter

One more interesting thing that many people might find useful to apply to their GUIs is displaying images. Let's modify the previous example a little bit.

import tkinter  
import tkinter.filedialog

root = tkinter.Tk()

def display_image():  
    f = tkinter.filedialog.askopenfilename(
        parent=root, initialdir='C:/Tutorial',
        title='Choose file',
        filetypes=[('png images', '.png'),
                   ('gif images', '.gif')]
        )

    new_window = tkinter.Toplevel(root)

    image = tkinter.PhotoImage(file=f)
    l1 = tkinter.Label(new_window, image=image)
    l1.image = image
    l1.pack()

b1 = tkinter.Button(root, text='Display image', command=display_image)  
b1.pack(fill='x')

root.mainloop()  

Output:

Let's see what changed inside the function executed by our button, now renamed to display_image. We display the File Dialog, we use the same criteria for file selection as before, and again we store the returned path in variable f. However, after getting the file path, we don't print it in the console. What we do is we create a top-level window in line 14. Then, in line 16, we instantiate an object of the PhotoImage class, by making it read the .png file that was selected by the user. The object is then stored in the image variable, which we can pass as an argument for the construction of Label widget in line 17. In line 18, we make sure to keep a reference to the image object in order to keep it from getting cleared by Python's garbage collector. Then, in line 19, we pack our label (this time displaying an image, not a text) inside the new_window.

Color Chooser

Another common feature, especially in software focused on graphics, is allowing the user to select a color from a palette. In this case, Tkinter also offers a nice, ready-to-use solution that should satisfy most of our needs concerning the color choosing feature.

import tkinter  
import tkinter.colorchooser

root = tkinter.Tk()

def color_button():  
    color = tkinter.colorchooser.askcolor(parent=root)
    print(color)
    b1.configure(bg=color[1])

b1 = tkinter.Button(root, text='Select Color', command=color_button)  
b1.pack(fill='x')

root.mainloop()  

Output:

In line 2 of the example shown above, we import a class called colorchooser. We use its askcolor() method in line 7. This method, similarly to askopenfilename(), is responsible for opening a nice, complex dialog (a color chooser in this case) and returns the data dependent on the user's choice. In this case, after the user picks a color from the palette and accepts their choice, the object returned to variable color is a tuple containing two elements. The first element is a tuple that stores values for the selected color's red, green and blue channels. The second element of the tuple is the same color specified in hexadecimal format. We can see the contents of the tuples in our console, thanks to the print() in line 8.

After we store the tuple returned by askcolor in variable color, we then use that variable in line 9 to configure the b1 button. As you already know, the bg argument is responsible for controlling the button's background color. We pass the first element of the color tuple to it (the color representation in hexadecimal format). As a result, after pressing the b1 button, the user can change its background color using a nice color chooser.

Message Boxes

Before move on from Tkinter to Python Mega Widgets, it's good to mention one more feature of the Tkinter module that makes programming GUIs a bit quicker. Tkinter offers so-called Message Boxes, which are a set of simple, but widely used standard dialogs. These message boxes can be used to display a quick message, a warning, or when we need our user to make a simple yes/no decision. The following example demonstrates all message boxes offered by Tkinter:

import tkinter  
import tkinter.messagebox

root = tkinter.Tk()

def display_and_print():  
    tkinter.messagebox.showinfo("Info","Just so you know")
    tkinter.messagebox.showwarning("Warning","Better be careful")
    tkinter.messagebox.showerror("Error","Something went wrong")

    okcancel = tkinter.messagebox.askokcancel("What do you think?","Should we go ahead?")
    print(okcancel)

    yesno = tkinter.messagebox.askyesno("What do you think?","Please decide")
    print(yesno)

    retrycancel = tkinter.messagebox.askretrycancel("What do you think?","Should we try again?")
    print(retrycancel)

    answer = tkinter.messagebox.askquestion("What do you think?","What's your answer?")
    print(answer)

b1 = tkinter.Button(root, text='Display dialogs', command=display_and_print)  
b1.pack(fill='x')

top.mainloop()  

Output:

This time, our b1 button executes function display_and_print(). The function lets 7 message boxes pop up in sequence - each is displayed after the user interacts with the previous one. dialogs defined in lines 11 - 21 are dialogs that require the user to choose one of two available options - therefore, they return values based on the decisions and store them in their respective variables. In each case, we can pass two arguments while defining the dialogs - first is always the dialog's title, and the second one contains contents of its main message.

So, to start from the top. In line 7, we define a simple showinfo dialog, which is only meant to display a neutral icon, a message, and an OK button that closes it. In lines 8 and 9, we have similar, simple types of message boxes, but their icons indicate that caution from the user is required (showwarning) or that an error has occurred (showerror). Note that in each of the three cases, a different sound is played upon the dialog's appearance.

As I stated before, lines 11 - 21 contain code responsible for displaying dialogs to get the user's decision. askokcancel (line 11) returns True if the user clicks OK and False if they click Cancel. askyesno (line 14) returns True if the user clicks Yes and False if the user clicks No. askretrycancel (line 17) returns True if the user clicks Retry and False if the user clicks Cancel. askquestion is very similar to askyesno, but returns 'yes' if the user clicks Yes and 'no' if the user clicks No.

Keep in mind, that the exact appearance of the File Dialog, Color Chooser, and all Message Boxes depends on the operating system that the code is executed on, as well as on the system language.

Progress Bar

Another useful element of advanced GUIs is a Progress Bar. The following example shows a simple implementation of this feature using Tkinter:

import tkinter  
import time  
from tkinter import ttk

root = tkinter.Tk()

def start():  
    for k in range(1, 11):
        progress_var.set(k)
        print("STEP", k)
        k += 1
        time.sleep(1)
        root.update_idletasks()

b1 = tkinter.Button(root, text="START", command=start)  
b1.pack(side="left")

progress_var = tkinter.IntVar()

pb = ttk.Progressbar(root, orient="horizontal",  
                     length=200, maximum=10,
                     mode="determinate",
                     var=progress_var)
pb.pack(side="left")

pb["value"] = 0

root.mainloop()  

Output:

The example above shows the implementation of Progressbar. It is a part of the tkinter.ttk module, which provides access to the Tk themed widget set, introduced in Tk 8.5. This is why we need to additionally import the ttk module in line 3.

Our progress bar's state will be controlled by time - the bar will progress in ten steps, executed in one-second intervals. For that purpose, we import the time module in line 2.

We define our Progressbar in line 20. We define its parent widget (root), we give it a "horizontal" orientation and a length of 200 pixels. Then, we define the maximum value - which is the value of the variable assigned to the progress bar using the var argument (in our case, the progress_var variable), that means the progress bar is filled completely. We set the mode to "determinate", which means that our code will be moving the indicator's length to precisely defined points based on the progress_var's value.

The progress_var integer variable that will control the bar's progress is defined in line 18. In line 26, using a dictionary-like assignment, we set the initial value of the progress bar to 0.

In line 15, we create a Button that's supposed to start the clock controlling our bar's progress by executing the start() function, defined between lines 7 and 13. There, we have a simple for loop, that will iterate through values between 1 and 10. With each iteration, the progress_var value is updated and increased by 1. In order to be able to observe the progress clearly, we wait for one second during each iteration (line 12). We then use the root window's update_idletasks() method in line 13, in order to let the program update the appearance of the progress bar even though we're still executing the for loop (so, we're technically still in a single mainloop() iteration).

Python Mega Widgets

If you use Tkinter extensively in your projects, I think it's a good idea to consider incorporating Python Mega Widgets in your code. Python Mega Widgets is a toolkit based on Tkinter that offers a set of megawidgets: complex, functional and relatively aesthetically pleasing widgets made out of simpler Tkinter widgets. What's great about this package, which you can download here is that the general philosophy of defining and orienting widgets is the same as in case of Tkinter, and you can mix both libraries in your code. Let's finish our tutorial by scratching the surface of this powerful toolkit.

EntryField Widget

One of the most useful widgets of the Pmw package is EntryField. Let's analyze the following example to see what it's capable of:

import tkinter  
import Pmw

root = tkinter.Tk()

def color_entry_label():  
    color = entry_color.get()
    entry_number.configure(label_bg=color)

entry_color = Pmw.EntryField(root, labelpos="w",  
                                 label_text="First name:",
                                 entry_bg="white",
                                 entry_width=15,
                                 validate="alphabetic")

entry_number = Pmw.EntryField(root, labelpos="w",  
                                 label_text="Integer:",
                                 entry_bg="white",
                                 entry_width=15,
                                 validate="integer")

ok_button = tkinter.Button(root, text="OK", command=color_entry_label)

entry_color.pack(anchor="e")  
entry_number.pack(anchor="e")  
ok_button.pack(fill="x")

root.mainloop()  

Output:

This time, we have to not only import tkinter, but also our freshly installed Pmw package (line 2). As always, we use the Tk class to initiate our root window.

In lines 10-14 and 16-20 we define two Pmw.EntryField widgets. An EntryField is a functional mix of Tkinter's Label and Entry, with some addition of useful functionalities. The first argument for the widget's initialization is, of course, the parent widget. The label_text, entry_bg and entry_width control some self-explanatory aspects of the widget's appearance. The most interesting argument in our example is probably the validate argument. Here, we can decide what kind of data the user can put inside the field.

In the entry_color field, we expect a string of letters, so we set validate to "alphabetic". In the entry_number widget, we expect an integer, and that's what we set the validate argument value to. This way, if we try to put a number inside the former and a letter inside the latter, the symbols simply won't appear in the widgets and a system sound will be played, informing us that we're trying to do something wrong. Also, if the widget expects a certain type of data and its contents are in conflict with this condition the moment it's initialized, the EntryField will be highlighted red.

As you can see in our example, right after we display our window, the first entry field is white, and the second one is red. This is because an empty string (default content of the entries) falls into the category of "alphabetic" entities, but it's definitely not an integer.

The button defined in line 26 executes the color_entry_label() command defined between lines 6 and 8. The function's goal is to paint the entry_number widget label's background according to the contents of the entry_color widget. In line 7, the get() method is used to extract the contents of the entry_color EntryField. Then, naturally, the configure() method is used in order to change the appearance of the entry_number widget. Note that in order to change characteristics of Pmw widgets that are composed of several simpler widgets, we have to specify which sub-widget we want to configure (in our case, it's the label - this is why we configure the label_bg and not, let's say, the entryfield_bg).

The EntryField widget might not be visually very impressive, but even this simple example illustrates the potential of mega-widgets - building this kind of self-verifying piece of the interface of higher complexity would require much more code if we tried to achieve the same effect using plain Tkinter. I encourage you to explore other powerful mega-widgets described in the toolkit's documentation.

Conclusion

Tkinter is one of many available GUI libraries for Python, but its great advantage is that it's considered a Python standard and still distributed, by default, with all Python distributions. I hope you enjoyed this little tutorial and now have a good grasp on building interfaces for users that might get scared off by command-line operated software.

Author image
Poznan, Poland
I use Python to develop bioinformatics tools that help biologists build and visualize structural models of important molecular machines.