A. Tolia
A. Tolia

Reputation: 33

Update Tkinter GUI from a separate thread running a command

I'm trying to pack something in Tkinter from a separate thread. I keep getting a RuntimeError.

Specifically: RuntimeError: main thread is not in main loop

The question is: How do I update the GUI (mainloop) from a separate thread?

The source of the issue:

    def protocol(self, scheduleName): #ran as thread
        print("Getting courses")
        self.coursesLabel.pack() #throws the error
        ...

I want to be able to pack items from this thread to mainloop(). I'm not sure how to transfer this between two threads.

Thanks in advance for the help!

EDIT: Most of these questions that already exist respond to people who are running two separate GUIs simultaneously. I just need to know how to pack things from one thread to another, not something as complex as running two GUIs at the same time. Most answers also used Queues, and I was wondering if there was a more elegant way to pack things from separate threads.

Upvotes: 3

Views: 7202

Answers (1)

Mike67
Mike67

Reputation: 11342

In tkinter, you can submit an event from a background thread to the GUI thread using event_generate. This allows you to update widgets without threading errors.

  1. Create the tkinter objects in the main thread
  2. Bind the root to a vitual event (ie << event1 >>), specifying an event handler. Arrow brackets are required in the event name.
  3. Start the background thread
  4. In the background thread, use event_generate to trigger the event in the main thread. Use the state property to pass data (number) to the event.
  5. In the event handler, process the event

Here's an example:

from tkinter import *
import datetime
import threading
import time

root = Tk()
root.title("Thread Test")
print('Main Thread', threading.get_ident())    # main thread id

def timecnt():  # runs in background thread
    print('Timer Thread',threading.get_ident())  # background thread id
    for x in range(10):
        root.event_generate("<<event1>>", when="tail", state=123) # trigger event in main thread
        txtvar.set(' '*15 + str(x))  # update text entry from background thread
        time.sleep(1)  # one second

def eventhandler(evt):  # runs in main thread
    print('Event Thread',threading.get_ident())   # event thread id (same as main)
    print(evt.state)  # 123, data from event
    string = datetime.datetime.now().strftime('%I:%M:%S %p')
    lbl.config(text=string)  # update widget
    #txtvar.set(' '*15 + str(evt.state))  # update text entry in main thread

lbl = Label(root, text='Start')  # label in main thread
lbl.place(x=0, y=0, relwidth=1, relheight=.5)

txtvar = StringVar() # var for text entry
txt = Entry(root, textvariable=txtvar)  # in main thread
txt.place(relx = 0.5, rely = 0.75, relwidth=.5, anchor = CENTER)

thd = threading.Thread(target=timecnt)   # timer thread
thd.daemon = True
thd.start()  # start timer loop

root.bind("<<event1>>", eventhandler)  # event triggered by background thread
root.mainloop()
thd.join()  # not needed

Output (note that the main and event threads are the same)

Main Thread 5348
Timer Thread 33016
Event Thread 5348
......

I added an Entry widget to test if the StringVar can be updated from the background thread. It worked for me, but you can update the string in the event handler if you prefer. Note that updating the string from multiple background threads could be a problem and a thread lock should be used.

Note that if the background threads exits on its own, there is no error. If you close the application before it exits, you will see the 'main thread' error.

Upvotes: 4

Related Questions