0

Is it possible to get the GUI to update variable 'a' to update in real time while 'thread2' increments the value?

import tkinter as tk
from threading import Thread
import time


a = 0  # global variable

def thread1(threadname):
    root = tk.Tk()
    w = tk.Label(root, text=a)
    w.pack()
    root.mainloop()


def thread2(threadname):
    global a
    while True:
        a += 1
        time.sleep(1)


thread1 = Thread( target=thread1, args=("Thread-1", ) )
thread2 = Thread( target=thread2, args=("Thread-2", ) )

thread1.start()
thread2.start()

If I create a loop and print 'a' I get the correct result.

def thread1(threadname):
    global a
    while True:
        print(a)
 #   root = tk.Tk()
 #   w = tk.Label(root, text=a)
 #   w.pack()
 #   root.mainloop()

Any help would be appreciated

Dan
  • 23
  • 4

2 Answers2

1

When you create your label, that's not a "live" connection. That passes the current value of the variable a. You then enter your main loop, and that thread does nothing else until the application exits. You need to send the new value to a function that executes as part of the main thread, and that function will need access to the label.

This works:

import tkinter as tk
from threading import Thread
import time


class GUI(object):
    def __init__(self):
        self.a = 0 
        self.w = tk.Label(root, text=self.a)
        self.w.pack()
        thread2 = Thread( target=self.thread2, args=("Thread-2", ) )
        thread2.start()

    def thread2(self,threadname):
        while True:
            self.a += 1
            root.after_idle(self.update)
            time.sleep(1)

    def update(self):
        self.w.config(text=self.a)

root = tk.Tk()
gui = GUI()
root.mainloop()

It is possible to make a live connection using textvariable, but then you have to change the type of a to a tkinter.StringVariable. Check here: Update Tkinter Label from variable

Tim Roberts
  • 48,973
  • 4
  • 21
  • 30
  • Just a quick question: Is `.after_idle` thread safe? I read that it's similar to `.after` but is it safe enough to use from multiple threads without warring about tkinter crashing? – TheLizzard Apr 20 '21 at 20:20
  • The whole POINT of the `after` APIs is to force the passed function to be called in the main thread. It basically IS the thread synchronizer. – Tim Roberts Apr 20 '21 at 20:27
  • Well I hadn't seen anyone use `after_idle` and I assumed `after` is to avoid the window freezing/becoming unresponsive when using `time.sleep` – TheLizzard Apr 20 '21 at 20:32
  • @TimRoberts Sorry, I'm definitely a python noob. Reading through the tkinter docs, I was under the impression that the program loops between the `Tk()` and the `mainloop()` indefinitely, updating each iteration. I guess that's what I don't understand is why the print command works, but my method doesn't Trying to use this as a learning experience for the future. When I can get to an IDE I'll try your technique. – Dan Apr 20 '21 at 21:13
  • 1
    @Dan The `.mainloop()` updates the window, handles all incomming events to their respective binded functions and handles all `.after` and `.after_idle` scripts. So actually inside the `mainloop` there is code that calls the function that is passed into `after_idle` in this case and `after` in my answer. – TheLizzard Apr 20 '21 at 21:42
  • Right. `mainloop` is just a very short loop that says `wait-for-event`, `fetch-event`, `dispatch-event`, and that's about it. `after_idle` just posts an event on to that queue, and the dispatcher knows how to process that event by calling the function. For what it's worth, this is how EVERY GUI framework works. There's always a main loop that looks like that. – Tim Roberts Apr 20 '21 at 22:17
  • Thanks so much for the responses, that makes a lot more sense. I will try it out in the morning. – Dan Apr 21 '21 at 01:47
  • Looks like both solutions work well, I agree though that threading makes this a lot more complicated and prone to failure so I think I'll stick to using `after` for simplicity sake. Thanks again @TimRoberts and @TheLizzard for the help! – Dan Apr 21 '21 at 11:29
0

Try using a <tkinter.Tk>.after loop like this:

import tkinter as tk

def loop():
    global a
    a += 1
    label.config(text=a)
    # run `loop` again in 1000 ms
    root.after(1000, loop)

a = 0
root = tk.Tk()
label = tk.Label(root, text=a)
label.pack()
loop() # Start the loop
root.mainloop()

I used a .after script because tkinter and threading don't go together very well. Sometimes tkinter can crash without even giving you an error message if you try to call some tkinter commands from the second thread.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31