Threads and Tkinter

Tkinter: How to use threads to preventing main event loop from freezing

When you join the new thread in the main thread, it will wait until the thread finishes, so the GUI will block even though you are using multithreading.

If you want to place the logic portion in a different class, you can subclass Thread directly, and then start a new object of this class when you press the button. The constructor of this subclass of Thread can receive a Queue object and then you will be able to communicate it with the GUI part. So my suggestion is:

  1. Create a Queue object in the main thread
  2. Create a new thread with access to that queue
  3. Check periodically the queue in the main thread

Then you have to solve the problem of what happens if the user clicks two times the same button (it will spawn a new thread with each click), but you can fix it by disabling the start button and enabling it again after you call self.prog_bar.stop().

import queue

class GUI:
# ...

def tb_click(self):
self.progress()
self.prog_bar.start()
self.queue = queue.Queue()
ThreadedTask(self.queue).start()
self.master.after(100, self.process_queue)

def process_queue(self):
try:
msg = self.queue.get_nowait()
# Show result of the task if needed
self.prog_bar.stop()
except queue.Empty:
self.master.after(100, self.process_queue)

class ThreadedTask(threading.Thread):
def __init__(self, queue):
super().__init__()
self.queue = queue
def run(self):
time.sleep(5) # Simulate long running process
self.queue.put("Task finished")

Sharing variables between threads in Python tkinter

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

Tkinter GUI, I/O & Threading: When to use queues, when events?

So I did it like this but I do not know if it fits to you or if this is a good way to do this, but it safes you the .after as stated in the comments, which has the benefit that your function do_stuff is just called when needed.

import tkinter as tk
import time
import threading

def get_data():
time.sleep(3)
print('sleeped 3')
_check.set(1)

def do_stuff():
try:
root.configure(bg='#'+str(_var.get()))
_var.set(_var.get()+101010)
except:
_var.set(101010)

root = tk.Tk()
_check = tk.IntVar(value=0)
_var = tk.IntVar(value=101010)

def callback(event=None, *args):
t1 = threading.Thread(target=get_data)
t1.start()

do_stuff()

_check.trace_add('write', callback) #kepp track of that variable and trigger callback if changed
callback() # start the loop

root.mainloop()

Some research:

[The Tcl]

interpreter is only valid in the thread that created it, and all Tk
activity must happen in this thread, also. That means that the
mainloop must be invoked in the thread that created the
interpreter. Invoking commands from other threads is possible;
_tkinter will queue an event for the interpreter thread, which will
then execute the command and pass back the result.

#l1493 var_invoke

 The current thread is not the interpreter thread.  Marshal

the call to the interpreter thread, then wait for

completion. */

if (!WaitForMainloop(self))

return NULL;

is-it-safe-to-use-a-intvar-doublevar-in-a-python-thread

When you set a variable, it calls the globalsetvar method on the
master widget associated with the Variable. The _tk.globalsetvar
method is implemented in C, and internally calls var_invoke, which
internally calls WaitForMainLoop, which will attempt schedule the
command for execution in the main thread, as described in the quote
from the _tkinter source I included above.

wiki.tcl

     Start
|
|<----------------------------------------------------------+
v ^
Do I have No[*] Calculate how Sleep for at |
work to do? -----> long I may sleep -----> most that much --->|
| time |
| Yes |
| |
v |
Do one callback |
| |
+-----------------------------------------------------------+

Commonsense

from bugtracker:

Tkinter and threads.

If you want to use both tkinter and threads, the safest method is to
make all tkinter calls in the main thread. If worker threads generate
data needed for tkinter calls, use a queue.Queue to send the data to
the main thread. For a clean shutdown, add a method to wait for
threads to stop and have it called when the window close button [X] is
pressed.

effbot

Just run all UI code in the main thread, and let the writers write to
a Queue object; e.g.

Conclusion

The Way you did it and the way I did it dosent seem like the ideal but they seem not wrong at all. It depends on what is needed.

Update Tkinter GUI from a separate thread running a command

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.

Best way to Multi-Thread Tkinter Front End with the Back end

My fix was to set the command to a lambda, then apply the threading. Also realized last minute I was placing the threading into the wrong button.

command=lambda : threading.Thread(target=getattr(Backend, cmdstr)).start())

Tkinter Threading Error: RuntimeError: threads can only be started once

Your code will only create one thread and assign its start function reference to command option. Therefore same start() function will be called whenever the button is clicked.

You can use lambda instead:

command=lambda: threading.Thread(target=self.okbutton).start()

Then whenever the button is clicked, a new thread will be created and started.



Related Topics



Leave a reply



Submit