How to wait for a server response without freezing the tkinter GUI

Viewed 72

I am making a tkinter GUI that requests information to a server that takes some time to respond. I really don't know how to tell tkinter to wait for the response in a clever way so that the window loop doesnt freeze.

What I want to achieve is to make the popup window responsive, and to see the animation of the progressbar. I don't really know if it helps but I intend to use this GUI on Windows.

Here is my code: (I used time.sleep to simulate sending and recieving from a server)

import tkinter as tk
import tkinter.ttk as ttk
import time


def send_request(data):
    # Sends request to server, manages response and returns it
    time.sleep(10)


class Window(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        # Options of the window
        self.geometry("500x250")
        self.resizable(False, False)
        self.grab_set()
        # Widgets of the window
        self.button = tk.Button(self, text="Send Request", command=self.start_calc)
        self.button.pack()
        self.bar = ttk.Progressbar(self, orient = "horizontal", mode= "indeterminate")
        self.bar.pack(expand=1, fill=tk.X)

    def start_calc(self):
        # Prepares some data to be send
        self.data_to_send = []
        # Start bar
        self.bar.start()
        # Call send request
        self.after(10, self.send_request_and_save_results)

    def send_request_and_save_results(self):
        # Send request with the data_to_send
        result = send_request(self.data_to_send)
        # Save results
        # Close window
        self.quit()
        self.destroy()


class App:
    def __init__(self, root):
        self.root = root
        self.button = tk.Button(root, text="Open Window", command=self.open_window)
        self.button.pack()

    def open_window(self):
        window = Window(self.root)
        window.mainloop()


root = tk.Tk()
root.geometry("600x300")
app = App(root)
root.mainloop()
3 Answers

I came up with this solution:

import tkinter as tk
import tkinter.ttk as ttk
import time
from threading import Thread


def send_request_and_save_results(data, flag):
    # This is called in another thread so you shouldn't call any tkinter methods
    print("Start sending: ", data)
    time.sleep(10)
    print("Finished sending")

    # Signal that this function is done
    flag[0] = True


class Window(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        # Options of the window
        self.geometry("500x250")
        self.resizable(False, False)
        self.grab_set()
        # Widgets of the window
        self.button = tk.Button(self, text="Send Request", command=self.start_calc)
        self.button.pack()
        self.bar = ttk.Progressbar(self, orient="horizontal", mode="indeterminate")
        self.bar.pack(expand=1, fill="x")

    def start_calc(self):
        # Prepares some data to be send
        self.data_to_send = [1, 2, 3]
        # Start bar
        self.bar.start()
        # Call send request
        self.send_request_and_save_results()

    def send_request_and_save_results(self):
        # Create a flag that wukk signal if send_request_and_save_results is done
        flag = [False]
        # Send request with the data_to_send and flag
        t1 = Thread(target=send_request_and_save_results,
                    args=(self.data_to_send, flag))
        t1.start()
        # A tkinter loop to check if the flag has been set
        self.check_flag_close_loop(flag)

    def check_flag_close_loop(self, flag):
        # if the flag is set, close the window
        if flag[0]:
            self.close()
        # Else call this function again in 100 milliseconds
        else:
            self.after(100, self.check_flag_close_loop, flag)

    def close(self):
        # I am pretty sure that one of these is unnecessary but it
        # depends on your program
        self.quit()
        self.destroy()


class App:
    def __init__(self, root):
        self.root = root
        self.button = tk.Button(root, text="Open Window", command=self.open_window)
        self.button.pack()

    def open_window(self):
        window = Window(self.root)
        window.mainloop()


root = tk.Tk()
root.geometry("600x300")
app = App(root)
root.mainloop()

Notice how all tkinter calls are in the main thread. This is because sometimes tkinter doesn't play nice with other threads.

All I did was call send_request_and_save_results with a flag that the function sets when it is done. I periodically chech that flag in the check_flag_close_loop method, which is actually a tkinter loop.

The flag is a list with a single bool (the simplest solution). That is because python passes mutable objects by reference and immutable objects by value.

Use threading module to run your code and get the request parallelly

Just call your function like this.

t1 = Thread(target=window.send_request_and_save_results)
t1.start()

Updated Code

import tkinter as tk
import tkinter.ttk as ttk
import time
from threading import Thread

def send_request(data):
    # Sends request to server, manages response and returns it
    time.sleep(10)


class Window(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        # Options of the window
        self.geometry("500x250")
        self.resizable(False, False)
        self.grab_set()
        # Widgets of the window
        self.button = tk.Button(self, text="Send Request", command=self.start_calc)
        self.button.pack()
        self.bar = ttk.Progressbar(self, orient = "horizontal", mode= "indeterminate")
        self.bar.pack(expand=1, fill=tk.X)

    def start_calc(self):
        # Prepares some data to be send
        self.data_to_send = []
        # Start bar
        self.bar.start()
        # Call send request
        self.after(10, self.send_request_and_save_results)

    def send_request_and_save_results(self):
        # Send request with the data_to_send
        result = send_request(self.data_to_send)
        # Save results
        # Close window
        self.quit()
        self.destroy()


class App:
    def __init__(self, root):
        self.root = root
        self.button = tk.Button(root, text="Open Window", command=self.open_window)
        self.button.pack()

    def open_window(self):
        window = Window(self.root)
        t1 = Thread(target=window.send_request_and_save_results)
        t1.start()
        window.mainloop()


root = tk.Tk()
root.geometry("600x300")
app = App(root)


root.mainloop()


I hope this will help you.

Thanks to @codester_09 suggestion of using threading.Thread I have managed to get it working but I don't really understand why. After many errors this is what I did.

Now I wonder if what I did is safe.

import tkinter as tk
import tkinter.ttk as ttk
import time
from threading import Thread


def send_request_and_save_results(data):
    global results
    print("Start sending: ", data)
    # Sends request to server, manages response and returns it
    time.sleep(10)
    print("Finished sending")
    # Save results
    results[0] = "Hello"
    results[1] = "Hola"


class Window(tk.Toplevel):
    def __init__(self, root, *args, **kargs):
        super().__init__(root, *args, **kargs)
        # Options of the window
        self.geometry("500x250")
        self.resizable(False, False)
        self.grab_set()
        # Widgets of the window
        self.button = tk.Button(self, text="Send Request", command=self.start_calc)
        self.button.pack()
        self.bar = ttk.Progressbar(self, orient = "horizontal", mode= "indeterminate")
        self.bar.pack(expand=1, fill=tk.X)

    def start_calc(self):
        # Prepares some data to be send
        self.data_to_send = [1, 2, 3]
        # Start bar
        self.bar.start()
        # Call send request
        t1 = Thread(target=self.send_request_and_save_results)
        t1.start()

    def send_request_and_save_results(self):
        # Send request with the data_to_send
        send_request_and_save_results(self.data_to_send)
        # Close window
        self.after(10, self.close)

    def close(self):
        self.quit()
        self.destroy()


class App:
    def __init__(self, root):
        self.root = root
        self.button = tk.Button(root, text="Open Window", command=self.open_window)
        self.button.pack()

    def open_window(self):
        window = Window(self.root)
        window.mainloop()

#
root = tk.Tk()
root.geometry("600x300")
app = App(root)
results = [None] * 10
root.mainloop()
Related