1

Trying to update tkinter 'textvariable' running on a thread with 'main' function variable. I implemented a thread based solution so the code following the tkinter mainloop can run: https://stackoverflow.com/a/1835036/15409926.

Please share how I can resolve this error. If this is not the simplest method to update 'textvariable', please share alternatives.

Code:

from tkinter import *
from threading import Thread

class GUI(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.start()

    def run(self):
        self.root = Tk()
        self.var = StringVar()
        self.var.set("Initiated")

        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()

        width = screen_width*.12
        height = screen_height
        x = screen_width - width
        y = screen_height*.025

        self.root.geometry('%dx%d+%d+%d' % (width, height, x, y))

        label = Label(self.root, textvariable= self.var)
        label.pack()

        self.root.mainloop()

gui = GUI()

def main():
    for i in range(1000):
        gui.var.set(f'Current Iteration: {i}')

if __name__ == '__main__':
    main()

Window doesn't update: Tkinter window displays initial 'textvariable'

Error:

Traceback (most recent call last):
  File "test.py", line 36, in <module>
    main()
  File "test.py", line 33, in main
    gui.var.set(f'Current Iteration: {i}')
AttributeError: 'GUI' object has no attribute 'var'
555
  • 35
  • 4
  • first of, don't run `tkinter` methods in separate threads. You can run all of the `tkinter` stuff not in the main thread, but don't separate out its methods to different threads, that can break it. Second directly about your issue. Put some `time.sleep` in the `main` function before starting the loop and in the loop (otherwise it is too fast) because it takes some time for the thread to start – Matiiss Oct 27 '21 at 16:48
  • in your example you could use `root.after(millisecond, function)` insread of `threading` - but if you have function which need more time then you should use `queue` to send new value to thread and it should use `root.after` to check if there is new value in `queue` and update value directly in thread. – furas Oct 27 '21 at 17:33
  • @furas The real function is long-running, so using after would keep pausing the function, right? – 555 Oct 27 '21 at 17:35
  • 1
    if you run long-rinning code then it should run in separated thread and it should use `queue` to send value to thread with GUI, and this thread should use after to check periodically if there is new info in `queue` and update values in GUI - so it will update it in the same thread as GUI. – furas Oct 27 '21 at 17:37

1 Answers1

2

Most GUIs don't like to change values in widgets in separate thread.

You should rather use queue to send value to thread and it should use root.after(time, function) to run periodically function which will get value from queue and update value in GUI.

import tkinter as tk  # PEP8: `import *` is not preferred
from threading import Thread
import queue
import time  # simulate show program

class GUI(Thread):
    
    def __init__(self, queue):
        super().__init__()
        self.queue = queue
        self.start()

    def run(self):
        self.root = tk.Tk()
        self.var = tk.StringVar()
        self.var.set("Initiated")

        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()

        width = int(screen_width*.12)
        height = int(screen_height)
        x = int(screen_width - width)
        y = int(screen_height*.025)

        self.root.geometry(f'{width}x{height}+{x}+{y}')

        label = tk.Label(self.root, textvariable=self.var)
        label.pack()
 
        # run first time after 100ms (0.1 second)
        self.root.after(100, self.check_queue)

        self.root.mainloop()

    def check_queue(self):
        #if not self.queue.empty():
        while not self.queue.empty():
            i = self.queue.get()
            self.var.set(f'Current Iteration: {i}')

        # run again after 100ms (0.1 second)
        self.root.after(100, self.check_queue)
        
def main():
    q = queue.Queue()
    gui = GUI(q)

    for i in range(1000):
        q.put(i)
        # simulate show program
        time.sleep(0.5)


if __name__ == '__main__':
    main()

PEP 8 -- Style Guide for Python Code

furas
  • 134,197
  • 12
  • 106
  • 148
  • the `width`, `height`, `x` and `y` should be converted to `int`, otherwise they seem to raise an error. Also in this particular case there is a slight issue with that the actual `for` loop finishes in a few milliseconds, while reading the queue (especially only every 100 ms) is too slow to keep up so the actual loop will finish but the counter will run for a couple of seconds – Matiiss Oct 27 '21 at 17:56
  • @Matiiss you are right with integer values. as for `100ms` you can try `10ms` and this is only example - real program may run slower. And you may use `while not self.queue.empty()` instead of `if not self.queue.empty():` to fast skip some values in queue. – furas Oct 27 '21 at 18:12
  • 1
    @Matiiss - I added `int()` for windows geometry and `while not self.queue.empty()` – furas Oct 27 '21 at 18:19
  • great, btw I actually had never thought of using `.empty` in a loop like that to quickly go through the queue so that is something I learned, unfortunately I can't up-vote twice :) – Matiiss Oct 27 '21 at 18:32
  • I don't see my GUI being updated with i values from the for loop. Can you confirm? – 555 Oct 27 '21 at 18:34
  • I can print the value sent by queue but I can't seem to set StringVar(). – 555 Oct 27 '21 at 22:05
  • my code works for me on Linux Mint, Python 3.8. Most GUIs don't update widgets at once because it could make to many redraws when it has to change many widgets - and window may blink/flick. They wait for end of function and then they redraw all widgets in one step. Maybe it send new values very fast and it has no time to leave `while not ...empty` and it can't fininsh `check_queue` and redraw window/widgets. Maybe you need `if` instead of `while` in `while not ...empty`. OR maybe you don't uses `after` to run `check_queue` but you use `while`-loop to repeate code - and then it can't redraw it. – furas Oct 28 '21 at 08:26