Background Tasks With Tk (tkinter)
During the development of a desktop application with the tkinter
standard module, it is usual to reach the situation where a heavy operation (i. e. a task which lasts at least two or three seconds) freezes our window and every widget within it. When this happens, the user is not allowed to interact with our application anymore, nor can our code make changes to the interface (like increasing the value of a progress bar). This might be the case, for example, when we try to download a file via HTTP, to open a big file, to send an email via SMTP, to execute a command via subprocess
, etc.
Let's consider the following code:
Here we have a window with a button to download a file via the urllib.request
standard module. When pressed, the button runs the download_file()
function. Inside this function, the heavy task occurs in line 16, where the r.read()
method is called, which is responsible for downloading the content of the remote file. Note that in line 9, before downloading the file, the code disables the download button. Later, in line 19, the button is re-enabled once the file has been downloaded. Nevertheless, when we run the code we see that the window gets freezed during the download process, so actually the user never sees the button disabled.
Note the difference with a disabled button:
But why is this happening? The answer is simple: since both tkinter
(specifically the mainloop()
function) and download_file()
run inside the same thread, while r.read()
(or any other heavy operation) has the attention of the CPU, Tk can not process nor answer to the other multiple events that take place in the window. This is why the application seems frozen.
Since the Python standard library includes the threading
module, which let us spawn new threads, we could use it to move the download_file()
function to a standalone thread, and this way it would not block the program's main thread, where Tk runs. But it is not that easy. Tk widgets can only be modified from the same thread where the mainloop()
functions runs (i. e. Tk is not thread-safe). Thus, we would not be able to change the text of info_label
nor disable and then re-enable the download_button
from the new thread (at least not in a safe way).
One workaround for this issue is the following: to only move into the new thread the lines of download_file()
that are related to the file download (mainly r.read()
, which is the heavy task) and to leave in the main thread those other Tk-related parts (such as modifying the widgets' state). But how does the main thread know when the new thread has finished? Well, there is a function for that: threading.Thread.is_alive()
. Then, it seems like we can check every certain period of time where the new thread has finished in order to know if we need to re-enable the button (and other operations we would like to execute once the heavy task is over). To check every certain period of time if the heavy task is complete, we will need to use Tk's after()
, that let us schedule the execution of a function after a certain period of time has lapsed. This way we won't require a custom loop that would anyway block the application.
Without further ado, here's the code:
Preview:
You can use this code as a template to move any heavy task into the download_file_worker
(consider changing the name accordingly), and then put inside check_if_done()
those operations that must be run after the former function has terminated.
Download: file-downloader-tkinter-threading.zip.