Home > Mobile >  Tkinter, threads, several widgets to update periodically
Tkinter, threads, several widgets to update periodically

Time:11-04

I am trying to do an app using tkinter that gets information from some internet sources after entering stuff in an input box and then clicking a "load" button.

The button function reads the stuff string from the input box. After its loaded it retrieves some info from the internet (which is blocking), and then update some labels with this information.

Obviously, when clicking the load button, the app freezes a micro second due to the requests blocking the flow of the program. Once the info is retrieved, and the labels updated, some other labels would need to keep retrieving data from the internet constantly. To do this I have them done a bit like this:

Note: the print statements are done for testing, so I can see them on the console

def update_price_label(self):
    # TODO fix .after() duplicate
    print("Updating stuff")
    price = self.get_formatted_price(self.stuff) # this is another function being called, passing an argument of the stuff that has been loaded by the load button, this function returns price
    self.PriceValue.configure(text=price) # updates the price label with the price obtained from the function above
    
    self.PriceValue.after(1000, self.update_price_label) # sets a scheduler to run this function to update the price label each second

Above there is a function that is called upon clicking "load" for a label that needs to be updated all the time, this function calls another function that receives an argument, and then returns the price. Then the label is updated, and then its scheduled in an endless loop using the after() method of the priceValue label control. This way, the price gets updated all the time

def get_formatted__price(self, stuff):
    price = RETRIEVE PRICE # this is not a real function, but this is the request being done to the server to get the price
    return f"{price:.12f} # sets decimal numbers

This function is called by update_price_label(), receives an argument and returns the price

As you can see I have divided the label update functions vs the actual functions that actually retrieve the info from a server. So the first function is responsible for calling another function to retrieve the information, update the label, and then reschedule itself again using the after() method of each label widget.

There are like 5 functions like this that need to update several labels on the app, connecting to different sources and keeping the information up to date. All of them are scheduled using after() and run on the same intervals (1 second).

Obviously, due to not using any threading, the app freezes a lot when information is being requested due to them being blocking in nature.

So I need to implement threading, or any form of concurrency. And I am unable to find any good tutorials on this, or at least that fits my needs for an app that fetches information from sources on a regular basis.

I am still grasping the concepts of threading and concurrency in general, and maybe there are other ways such as asynchronism or other methods of concurrency that I don't know yet and might be better suited. But Threading seems to be the one that seems to be mostly used with Tkinter.

I assume each of these request functions would need a thread. Like this:

get_formatted_price_thread = Thread(target=get_formatted_price, args=(stuff), daemon=True) # calling a thread with an argument on the function and setting daemon to true, so it closes when I close the app

So I have tried to create threads on one of them as an example and I have found a few limitations such as:

There's no way to get the returned value of the get_formatted_price() function directly. So the other way could be just letting the function in a thread to change the label value. Or wrapping the whole label update function in a thread. But as I read everywhere, Tkinter is not thread safe. Meaning that updating tkinter widgets may work fine on some operative systems but not in others.

Besides that, I seem to struggle on to how to turn the structure of this app to something that works well with threading or any kind of concurrency. As I need to have all the labels updated as soon as new info is retrieved.

My other concern is that threads are controlled by the operative system. As it is the one that decides when threads are launched, and how that could impact my apps performance when fetching data.

I have also checked queues and the Queue library, but I am not sure if this what would help me, as each price update would be put into the queue by the thread, and retrieved by the label widget. But the information could be outdated as the queue gets the first element of the queue.

So my question here is what would I need to change here to achieve what I need. And if threading is what I need to go on, or if maybe I would need to try another approach.

Any example source code of apps that do what I need to would be very appreciated. After all, retrieving information, and keeping widgets up to date with that information should be like a pretty common use case.

Another approach I have thought of is creating a data structure, such as a Python dictionary, or an object. Each server fetching function would run in a thread in an endless loop as a daemon, and would write to the dictionary. Then the label widget update functions, since they are scheduled, would read the data on the dictionary and update the labels accordingly. But I think this approach might be messy, and probably there would be a delay on updating the labels vs the information on the dictionaries, unless a smaller after() scheduler timer is set. Or maybe all solutions are messy by default

Thank you.

CodePudding user response:

I would solve this by creating a data structure, creating a function that can update the display based on the current values in the data structure, and then binding to an event that calls this function. Then, create a thread that updates this data structure and emits the event when the data changes.

Here's a contrived example that calls a web service once a second and updates a simple data structure with the time and timezone information. Whenever the data changes it emits a <<Tick>> event that triggers an update of the display.

I'm not an expert on writing threaded tkinter code, and it's my understanding that except in a very few circumstances it is unsafe to run any tkinter code in a thread other than the one where the widgets were created. One exception is that it's safe for additional threads to generate events, since the events themselves get handled in the main GUI thread. I'm guessing it's also safe to call the winfo_exists function since it doesn't modify any internal data structures.

This example kills itself after 10 seconds so as to not hammer the server for too long.

import requests
import tkinter as tk
from tkinter.font import Font
from threading import Thread
import time


class ThreadedClock(tk.Frame):
    data = {"time": "", "tz": ""}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.time_label = tk.Label(self, width=12, font=Font(size=32))
        self.tz_label = tk.Label(self, text="GMT")

        self.time_label.pack(side="top", fill="x")
        self.tz_label.pack(side="top", fill="x")

        # call the refresh function on every <<Tick>> event
        self.bind("<<Tick>>", self.refresh)

        # start a thread to update the data and generate <<Tick>> events
        self.thread = Thread(target=self.get_data, daemon=True)

        self.running = True
        self.thread.start()

    def get_data(self):
        while self.winfo_exists():
            now = time.time()
            response = requests.get(
                "https://timeapi.io/api/Time/current/zone?timeZone=GMT"
            )
            t = response.json()

            timestr = f"{t['hour']:02}:{t['minute']:02}:{t['seconds']:02}"
            self.data = {"time": timestr, "tz": t["timeZone"]}
            self.event_generate("<<Tick>>")

            delta = time.time() - now
            time.sleep(delta)

    def refresh(self, event=None):
        self.time_label.configure(text=self.data["time"])
        self.tz_label.configure(text=f"timezone: {self.data['tz']}")


if __name__ == "__main__":
    root = tk.Tk()
    root.after(10000, root.destroy)

    clock = ThreadedClock(root)
    clock.pack(fill="both", expand=True)

    root.mainloop()
  • Related