Search code examples
pythontkinterfreeze

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


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()

Solution

  • 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.