How to build a Tkinter application using an object oriented approach

In a previous tutorial we saw the basic concepts behind the usage of Tkinter, a library used to create graphical user interfaces with Python. In this article we see how to create a complete although simple application. In the process, we learn how to use threads to handle long running tasks without blocking the interface, how to organize a Tkinter application using an object oriented approach, and how to use Tkinter protocols.

In this tutorial you will learn:

  • How to organize a Tkinter application using an object-oriented approach
  • How to use threads to avoid blocking the application interface
  • How to use make threads communicate by using events
  • How to use Tkinter protocols
How to build a Tkinter application using an object oriented approach
How to build a Tkinter application using an object oriented approach

Software requirements and conventions used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Distribution-independent
Software Python3, tkinter
Other Knowledge of Python and Object Oriented Programming concepts
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux-commands to be executed as a regular non-privileged user

Introduction

In this tutorial we will code a simple application “composed” of two widgets: a button and a progress bar. What our application will do, is just to download the tarball containing the latest WordPress release once the user clicks on the “download” button; the progress bar widget will be used to keep track of the download progress. The application will be coded by using an object oriented approach; in the course of the article I will assume the reader to be familiar with OOP basic concepts.

Organizing the application

The very first thing we need to do in order to build our application is to import the needed modules. For starters we need to import:

  • The base Tk class
  • The Button class we need to instantiate to create the button widget
  • The Progressbar class we need to create the progress bar widget

The first two can be imported from the tkinter module, while the latter, Progressbar, is included in the tkinter.ttk module. Let’s open our favorite text editor and start writing the code:

#!/usr/bin/env python3

from tkinter import Tk, Button
from tkinter.ttk import Progressbar



We want to build our application as a class, in order to keep data and functions well organized, and avoid cluttering the global namespace. The class representing our application (let’s call it WordPressDownloader), will extend the Tk base class, which, as we saw in the previous tutorial, is used to create the “root” window:

class WordPressDownloader(Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title('Wordpress Downloader')
        self.geometry("300x50")
        self.resizable(False, False)

Let’s see what the code we just wrote does. We defined our class as a subclass of Tk. Inside its constructor we initialized the parent, than we set our application title and geometry by calling the title and geometry inherited methods, respectively. We passed the title as argument to the title method, and the string indicating the geometry, with the <with>x<height> syntax, as argument to the geometry method.

We than set the root window of our application as non-resizable. We achieved that by calling the resizable method. This method accepts two boolean values as arguments: they establish whether the width and height of the window should be resizable. In this case we used False for both.

At this point, we can create the widgets which should “compose” our application: the progress bar and the “download” button. We add the following code to our class constructor (previous code omitted):

# The progressbar widget
self.progressbar = Progressbar(self)
self.progressbar.pack(fill='x', padx=10)

# The button widget
self.button = Button(self, text='Download')
self.button.pack(padx=10, pady=3, anchor='e')

We used the Progressbar class to create the progress bar widget, and than called the pack method on the resulting object to create a minimum of setup. We used the fill argument to make the widget occupy all the available width of the parent window (x axis), and the padx argument to create a margin of 10 pixels from its left and right borders.

The button was created by instantiating the Button class. In the class constructor we used the text parameter to set the button text. We than setup the button layout with pack: with the anchor parameter we declared that the button should be kept at the right of the main widget. The anchor direction is specified by using compass points; in this case, the e stands for “east” (this can be also specified by using constants included in the tkinter module. In this case, for example, we could have used tkinter.E). We also set the same horizontal margin we used for the progress bar.

When creating the widgets, we passed self as the first argument of their classes constructors in order to set the window represented by our class as their parent.

We didn’t define a callback for our button yet. For now, let’s just see how our application looks. In order to do that we have to append the main sentinel to our code, create an instance of the WordPressDownloader class, and call the mainloop method on it:

if __name__ == '__main__':
    app = WordPressDownloader()
    app.mainloop()

At this point we can make our script file executable and launch it. Supposing the file is named app.py, in our current working directory, we would run:

$ chmod +x app.py
./app.py

We should obtain the following result:

First look at our downloader application
First look at our downloader application

All seems good. Now let’s make our button do something! As we saw in the basic tkinter tutorial, to assign an action to a button, we must pass the function we want to use as callback as the value of the command parameter of the Button class constructor. In our application class, we define the handle_download method, write the code which will perform the download, and than assign the method as the button callback.

To perform the download, we will make use of the urlopen function which is included in the urllib.request module. Let’s import it:

from urllib.request import urlopen

Here is how we implement the handle_download method:

def handle_download(self):
    with urlopen("https://wordpress.org/latest.tar.gz") as request:
        with open('latest.tar.gz', 'wb') as tarball:
            tarball_size = int(request.getheader('Content-Length'))
            chunk_size = 1024
            read_chunks = 0

            while True:
                chunk = request.read(chunk_size)
                if not chunk:
                    break

                read_chunks += 1
                read_percentage = 100 * chunk_size * read_chunks / tarball_size
                self.progressbar.config(value=read_percentage)

                tarball.write(chunk)

The code inside the handle_download method is quite simple. We issue a get request to download the latest WordPress release tarball archive and we open/create the file we will use to store the tarball locally in wb mode (binary-write).

To update our progress bar we need to obtain the amount of downloaded data as a percentage: in order to do that, first we obtain the total size of the file by reading the value of the Content-Length header and casting it to int, than we establish that the file data should be read in chunks of of 1024 bytes, and keep the count of chunks we read using the read_chunks variable.



Inside the infinite while loop, we use the read method of the request object to read the amount of data we specified with chunk_size. If the read methods returns an empty value, it means that there is no more data to read, therefore we break the loop; otherwise, we update the amount of chunks we read, calculate the download percentage and reference it via the read_percentage variable. We use the computed value to update the progress bar by calling its config method. Finally, we write the data to the local file.

We can now assign the callback to the button:

self.button = Button(self, text='Download', command=self.handle_download)

It looks like everything should work, however, once we execute the code above and click on the button to start the download, we realize there is a problem: the GUI becomes unresponsive, and the progress bar is updated all at once when the download is completed. Why this happens?

Our application behaves this way since the handle_download method runs inside the main thread and blocks the main loop: while the download is being performed, the application cannot react to user actions. The solution to this problem is to execute the code in a separate thread. Let’s see how to do it.

Using a separate thread to perform long-running operations

What is a thread? A thread is basically a computational task: by using multiple threads we can make specific parts of a program be executed independently. Python makes very easy to work with threads via the threading module. The very first thing we need to do, is to import the Thread class from it:

from threading import Thread

To make a piece of code be executed in a separate thread we can either:

  1. Create a class which extends the Thread class and implements the run method
  2. Specify the code we want to execute via the target parameter of the Thread object constructor

Here, to make things better organized, we will use the first approach. Here is how we change our code. As a first thing, we create a class which extends Thread. First, in its constructor, we define a property which we use to keep track of the download percentage, than, we implement the run method and we move the code which performs the tarball download in it:

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0

    def run(self):
        with urlopen("https://wordpress.org/latest.tar.gz") as request:
            with open('latest.tar.gz', 'wb') as tarball:
                tarball_size = int(request.getheader('Content-Length'))
                chunk_size = 1024
                read_chunks = 0

                while True:
                    chunk = request.read(chunk_size)
                    if not chunk:
                        break

                    read_chunks += 1
                    self.read_percentage = 100 * chunk_size * read_chunks / tarball_size

                    tarball.write(chunk)

Now we should change the constructor of our WordPressDownloader class so that it accepts an instance of DownloadThread as argument. We could also create an instance of DownloadThread inside the constructor, but by passing it as argument, we explicitly declare that WordPressDownloader depends on it:

class WordPressDownloader(Tk):
    def __init__(self, download_thread, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.download_thread = download_thread
        [...]

What we want to do now, is to create a new method which will be used to keep track of the percentage progress and will update the value of the progress bar widget. We can call it update_progress_bar:

def update_progress_bar(self):
      if self.download_thread.is_alive():
          self.progressbar.config(value=self.download_thread.read_percentage)
          self.after(100, self.update_progress_bar)

In the update_progress_bar method we check if the thread is running by using the is_alive method. If the thread is running we update the progress bar with the value of the read_percentage property of the thread object. After this, to keep monitoring the download, we use the after method of the WordPressDownloader class. What this method does is to perform a callback after a specified amount of milliseconds. In this case we used it to re-call the update_progress_bar method after 100 milliseconds. This will be repeated until the thread is alive.

Finally, we can modify the content of the handle_download method which is invoked when the user clicks on the “download” button. Since the actual download is performed in the run method of the DownloadThread class, here we just need to start the thread, and invoke the update_progress_bar method we defined in the previous step:

def handle_download(self):
      self.download_thread.start()
      self.update_progress_bar()

At this point we must modify how the app object is created:

if __name__ == '__main__':
    download_thread = DownloadThread()
    app = WordPressDownloader(download_thread)
    app.mainloop()

If we now re-launch our script and start the download we can see that the interface is not blocked anymore during the download:

By using a separate thread the interface is not blocked anymore
By using a separate thread the interface is not blocked anymore



There is still a problem however. To “visualize” it, launch the script, and close the graphical interface window once the download has started but is not yet finished; do you see that there is something hanging the terminal? This happens because while the main thread has been closed, the one used to perform the download is still running (data is still being downloaded). How can we solve this problem? The solution is to use “events”. Let’s see how.

Using events

By using an Event object we can establish a communication between threads; in our case between the main thread and the one we are using to perform the download. An “event” object is initialized via the Event class we can import from the threading module:

from threading import Thread, Event

How does an event object work? An Event object has a flag which can be set to True via the set method, and can be reset to False via the clear method; its status can be checked via the is_set method. The long task executed in the run function of the thread we built to perform the download, should check the flag status before performing each iteration of the while loop. Here is how we change our code. First we create an event and bind it to a property inside the DownloadThread constructor:

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0
        self.event = Event()

Now, we should create a new method in the DownloadThread class, which we can use to set the flag of the event to False. We can call this method stop, for example:

def stop(self):
    self.event.set()

Finally, we need to add an additional condition in the while loop in the run method. The loop should be broken if there are no more chunks to read, or if the the event flag is set:

def run(self):
    [...]

    while True:
        chunk = request.read(chunk_size)
        if not chunk or self.event.is_set():
            break

        [...]

What we need to do now, is to call the stop method of the thread when the application window is closed, so we need to catch that event.

Tkinter protocols

The Tkinter library provides a way to handle certain events that happens to the application by using protocols. In this case we want to perform an action when the user clicks on the button to close the graphical interface. To achieve our goal we must “catch” the WM_DELETE_WINDOW event and run a callback when it is fired. Inside the WordPressDownloader class constructor, we add the following code:

self.protocol('WM_DELETE_WINDOW', self.on_window_delete)

The first argument passed to the protocol method is the event we want catch, the second is the name of the callback which should be invoked. In this case the callback is: on_window_delete. We create the method with the following content:

def on_window_delete(self):
    if self.download_thread.is_alive():
        self.download_thread.stop()
        self.download_thread.join()

    self.destroy()

As you can recall, the download_thread property of our WordPressDownloader class references the thread we used to perform the download. Inside the on_window_delete method we check if the thread has been started. If is the case, we call the stop method we saw before, and than the join method which is inherited from the Thread class. What the latter does, is blocking the calling thread (in this case the main one) until the thread on which the method is invoked terminates. The method accepts an optional argument which must be a floating point number representing the maximum number of seconds the calling thread will wait for the other one (in this case we don’t use it). Finally, we invoke the destroy method on our WordPressDownloader class, which kills the window and all the descendant widgets.



Here is the complete code we wrote in this tutorial:

#!/usr/bin/env python3

from threading import Thread, Event
from urllib.request import urlopen
from tkinter import Tk, Button
from tkinter.ttk import Progressbar

class DownloadThread(Thread):
    def __init__(self):
        super().__init__()
        self.read_percentage = 0
        self.event = Event()

    def stop(self):
        self.event.set()

    def run(self):
        with urlopen("https://wordpress.org/latest.tar.gz") as request:
            with open('latest.tar.gz', 'wb') as tarball:
                tarball_size = int(request.getheader('Content-Length'))
                chunk_size = 1024
                readed_chunks = 0

                while True:
                    chunk = request.read(chunk_size)
                    if not chunk or self.event.is_set():
                        break


                    readed_chunks += 1
                    self.read_percentage = 100 * chunk_size * readed_chunks / tarball_size

                    tarball.write(chunk)


class WordPressDownloader(Tk):
    def __init__(self, download_thread, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.download_thread = download_thread

        self.title('Wordpress Downloader')
        self.geometry("300x50")
        self.resizable(False, False)

        # The progressbar widget
        self.progressbar = Progressbar(self)
        self.progressbar.pack(fill='x', padx=10)

        # The button widget
        self.button = Button(self, text='Download', command=self.handle_download)
        self.button.pack(padx=10, pady=3, anchor='e')

        self.download_thread = download_thread
        self.protocol('WM_DELETE_WINDOW', self.on_window_delete)

    def update_progress_bar(self):
        if self.download_thread.is_alive():
            self.progressbar.config(value=self.download_thread.read_percentage)
            self.after(100, self.update_progress_bar)

    def handle_download(self):
      self.download_thread.start()
      self.update_progress_bar()

    def on_window_delete(self):
        if self.download_thread.is_alive():
            self.download_thread.stop()
            self.download_thread.join()

        self.destroy()


if __name__ == '__main__':
    download_thread = DownloadThread()
    app = WordPressDownloader(download_thread)
    app.mainloop()

Let’s open a terminal emulator and launch our Python script containing the above code. If we now close the main window when the download is still being performed, the shell prompt comes back, accepting new commands.

Summary

In this tutorial we built a complete graphical application using Python and the Tkinter library using an object oriented approach. In the process we saw how to use threads to perform long running operations without blocking the interface, how to use events to let a thread communicate with another, and finally, how to use Tkinter protocols to perform actions when certain interface events are fired.



Comments and Discussions
Linux Forum