0

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()
  • is it possible for you to send a request but not wait for a result? If so, can you write a function that quickly checks for a result but doesn't block if there is no result? You can then check once a second or once every half second for the result without blocking the GUI. – Bryan Oakley Sep 07 '22 at 19:16
  • @BryanOakley I dont really understand what you mean but I managed to achieve what I want somehow. No I just wonder if what I did makes sense and is safe. – Daniel Casasampera Sep 07 '22 at 19:30
  • Threading is safe, but it might be more complicated than it needs to be. – Bryan Oakley Sep 07 '22 at 20:12
  • @BryanOakley take a look at my response if you will and tell me what you think – Daniel Casasampera Sep 07 '22 at 20:51

3 Answers3

1

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.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • Thanks alot for your answer it makes perfect sense and I will implement it like you suggest in this answer. My only question is on the "safety" of my solution, why does ```.after(10, self.close)``` works but directly calling ```self.close()``` doesnt? Maybe you can call ```.after(10, self.close)``` in the thread because by doing that it will be the main thread (the tk mainloop) who exceutes the ```self.close()``` anyways. How could my solution fail? – Daniel Casasampera Sep 08 '22 at 10:04
  • In your code: the `.after(10, self.close)` tells the mainloop to run `self.close` in 0.01 seconds, so it runs from the main thread which is why you avoid the thread error. But there are some edge cases where even that isn't going to stop `tkinter` from crashing because you are calling `self.after` (a `tkinter` method) in the 2nd thread. – TheLizzard Sep 08 '22 at 10:08
  • For some reason my intuition tells me that ```.after``` method should be safe since it does not mutate anything, it just appends a cammand to be called by the mainloop when it can. Am I missing or misunderstanding something? Take a look at this [question](https://stackoverflow.com/questions/58118723/is-tkinters-after-method-thread-safe#:~:text=after%20is%20thread%2Dsafe%20because,(the%20most%20common%20case).). – Daniel Casasampera Sep 08 '22 at 10:25
  • Well, yes in most cases it is thread safe. But there are edge cases where it will still cause issues. Recently, I had a problem where just being able to access a `tkinter` widget from outside threads, caused an error without me even referencing the widget itself. To see an edge case look at [this](https://pastebin.com/wF5Fn19a). It crashes python itself - without even an error message (it doesn't actually use `.after` but still same principal). – TheLizzard Sep 08 '22 at 10:30
  • Thanks alot for your answers, very insightful for me. Just to be safe then I will not call ```.after``` method and instead I will do the checking loop like you suggest. Thanks alot! – Daniel Casasampera Sep 08 '22 at 10:50
  • I now wonder if this solution is thread safe. If the thread sets the flag var to True at the same time that the main thread tries to read it would that cause an issue? – Daniel Casasampera Sep 12 '22 at 22:49
  • It sets the flag as true at the end of the function. I don't think there is a problem with it. Are you getting errors/unexpected behaviour? Also all python variables are threadsafe in terms of writing/reading from them - for more info read about the python GIL. – TheLizzard Sep 12 '22 at 22:52
  • To be honest I'm not having any problems but I feel I am getting contradictory information. In one way if you build a flask app you shouldn't use global vars because of thread safety but then in the [docs it says this](https://docs.python.org/3/glossary.html#term-global-interpreter-lock). – Daniel Casasampera Sep 12 '22 at 23:04
  • If you are talking about something like [this](https://stackoverflow.com/questions/32815451/are-global-variables-thread-safe-in-flask-how-do-i-share-data-between-requests#:~:text=Global%20variables%20are%20still%20not,protection%20against%20most%20race%20conditions.), then you would have that same problem if you put the `flag[0] = True` at the start of the function - you can try it by adding `sleep(10); print("Done")` after it and look at the results. Thread safety comes in different forms - one of them is race conditions. – TheLizzard Sep 12 '22 at 23:10
  • I gess what I am wondering is that if the GIL is a thing why does the threading module have locks? Also in my case youre saying that i dont need to use locks like it points out in [this example](https://superfastpython.com/thread-race-condition-shared-variable/), right? (Sorry im learning along the way) – Daniel Casasampera Sep 12 '22 at 23:22
  • The GIL ensures that 2 threads can't write to a variable at the same time but doesn't ensure that each thread has the right value for the variable. In the example you linked, it is possible that python does `value = value + amount` instead of `value += amount`. Look at [this](https://www.youtube.com/watch?v=RY_2gElt3SA) video for an explanation. In my answer that shouldn't be a problem as only 1 thread writes to `flag[0]`. – TheLizzard Sep 13 '22 at 09:05
0

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.

codester_09
  • 5,622
  • 2
  • 5
  • 27
  • Unfortunatly this doesn't work, you cannot call the function ```send_request_and_save_results``` before ```start_calc```, the data to be send is prepared before, also it tells the progress bar to start the animation. – Daniel Casasampera Sep 07 '22 at 18:22
  • Also you shouldn't call `tkinter` methods from threads other than the one where the `tk.Tk` was created. – TheLizzard Sep 07 '22 at 23:13
0

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()
  • `tkitner` isn't thread safe, so you shouldn't call `tkinter` methods from threads other than the main one (where you created the `tk.Tk`). Also there is no need for the `.after`s in your code. I think you are misunderstanding how `.after` works. – TheLizzard Sep 07 '22 at 23:18
  • @TheLizzard if you don't use ```.after``` it won't work. without the first one, the progress bar wouldn't update, and without the second it gives a thread error. If threads is not an option how can it be done? – Daniel Casasampera Sep 07 '22 at 23:50
  • It gives you the thread error because you are calling `tkinter` methods from another thread. Right now the code is unsafe because the ` self.after(10, self.close)` is called from the 2nd thread. Also in this code, there is no difference between `self.after(10, t1.start)` and `t1.start()`. – TheLizzard Sep 08 '22 at 09:17