Threads and tkinter - python

I've heard that threads in Python are not easy to handle and they become more tangled with tkinter.
I have the following problem. I have two classes, one for the GUI and another for an infinite process. First, I start the GUI class and then the infinite process' class. I want that when you close the GUI, it also finishes the infinite process and the program ends.
A simplified version of the code is the following:
import time, threading
from tkinter import *
from tkinter import messagebox
finish = False
class tkinterGUI(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global finish
#Main Window
self.mainWindow = Tk()
self.mainWindow.geometry("200x200")
self.mainWindow.title("My GUI Title")
#Label
lbCommand = Label(self.mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
#Start
self.mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
class InfiniteProcess(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global finish
while not finish:
print("Infinite Loop")
time.sleep(3)
GUI = tkinterGUI()
GUI.start()
Process = InfiniteProcess()
Process.start()
When I click in the close button (in the upper right corner) the following error appears in the console:
Tcl_AsyncDelete: async handler deleted by the wrong thread
I don't know why it happens or what it means.

All Tcl commands need to originate from the same thread. Due to tkinter's
dependence on Tcl, it's generally necessary to make all tkinter gui statements
originate from the same thread. The problem occurs because
mainWindow is instantiated in the tkinterGui thread, but -- because mainWindow is an attribute of tkinterGui -- is not destroyed until tkinterGui is destroyed in the main thread.
The problem can be avoided by not making mainWindow an attribute of tkinterGui
-- i.e. changing self.mainWindow to mainWindow. This allows mainWindow to be destroyed when the run method ends in the tkinterGui thread. However, often you can avoid threads entirely by using mainWindow.after calls instead:
import time, threading
from tkinter import *
from tkinter import messagebox
def infinite_process():
print("Infinite Loop")
mainWindow.after(3000, infinite_process)
mainWindow = Tk()
mainWindow.geometry("200x200")
mainWindow.title("My GUI Title")
lbCommand = Label(mainWindow, text="Hello world", font=("Courier New", 16)).place(x=20, y=20)
mainWindow.after(3000, infinite_process)
mainWindow.mainloop()
If you want to define the GUI inside a class, you can still do so:
import time, threading
from tkinter import *
from tkinter import messagebox
class App(object):
def __init__(self, master):
master.geometry("200x200")
master.title("My GUI Title")
lbCommand = Label(master, text="Hello world",
font=("Courier New", 16)).place(x=20, y=20)
def tkinterGui():
global finish
mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
def InfiniteProcess():
while not finish:
print("Infinite Loop")
time.sleep(3)
finish = False
GUI = threading.Thread(target=tkinterGui)
GUI.start()
Process = threading.Thread(target=InfiniteProcess)
Process.start()
GUI.join()
Process.join()
or even simpler, just use the main thread to run the GUI mainloop:
import time, threading
from tkinter import *
from tkinter import messagebox
class App(object):
def __init__(self, master):
master.geometry("200x200")
master.title("My GUI Title")
lbCommand = Label(master, text="Hello world",
font=("Courier New", 16)).place(x=20, y=20)
def InfiniteProcess():
while not finish:
print("Infinite Loop")
time.sleep(3)
finish = False
Process = threading.Thread(target=InfiniteProcess)
Process.start()
mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finish = True
Process.join()

The fix here is simple, but hard to discover:
Call mainWindow.quit() immediately after mainwindow.mainloop(), so that the cleanup happens on the same thread as the one that created the tk UI, rather than on the main thread when python exits.

Related

python tkinter: Calling a objects method on button click does not work

here is my sample code:
from time import sleep
import tkinter as tk
import threading
class Action:
counter = 0
def do_something(self):
while True:
print('Looping')
sleep(5)
action = Action()
root = tk.Tk()
button = tk.Button(root, text='pressme harder', command=threading.Thread(target=action.do_something()).start())
button.grid(row=1, column=0)
root.mainloop()
What am I expecting?
I'm expecting that as soon as I click the button in the UI an new thread is running, which is looping in the background and does not interfere with the UI (or later maybe other threads doing tasks in the background)
What is really happening?
When running the code, the method of the class is executeds immdiately and locking the procedure. root.mainloop() is never reached and therefore no UI is drawn
Alternatively I tried the following change:
button = tk.Button(root, text='pressme harder', command=threading.Thread(target=lambda: action.do_something()).start())
This behaves in the following (imho wrong) way:
The method is also called immediately, without pressing the button. This time the UI is drawn but seems the be locked by the thread (UI is slow/stuttering, pressing the buttom does not work most of the time)
Any Idea whats wrong there? Or how do I handle this in a more stable way?
You shouldn't try to start a thread directly in the button command. I suggest you create another function that launches the thread.
from time import sleep
import tkinter as tk
import threading
class Action:
counter = 0
def do_something(self):
while True:
print('Looping')
sleep(2)
print("Finished looping")
def start_thread(self):
thread = threading.Thread(target=self.do_something, daemon=True)
thread.start()
action = Action()
root = tk.Tk()
button = tk.Button(root, text='pressme harder', command=action.start_thread)
button.grid(row=1, column=0)
root.mainloop()

How can I terminate a tkinter mainloop from a different thread

I have the following problem: I would like to create a GUI with tkinter, that reacts to signals, sent from a socket. For example, I would like to be able to terminate the application, when an end signal is received.
For that purpose I have a function, running in a separate thread, that listens for signals and acts accordingly. However, when I try to destroy the tkinter-GUI, the programm stops, and gives this error message:
Fatal Python error: PyEval_RestoreThread: the function must be called with the GIL held, but the GIL is released (the current Python thread state is NULL)
Python runtime state: initialized
I have recreated this minimum working example giving the same behavior:
import tkinter as tk
import time
import threading
class Gui(tk.Frame):
"""Minimal GUI with only a button"""
def __init__(self, master: tk.Tk):
tk.Frame.__init__(self, master)
self.pack()
tk.Button(self, text='Spam').pack()
class Client:
"""Client for handling signals"""
def __init__(self, master: tk.Tk):
self.master = master
self.gui = Gui(self.master)
self.signal = None # Initialize signal
self.thread = threading.Thread(target=self.listen_thread)
self.running = True
self.thread.start()
def listen_thread(self):
"""Listen for signals and handle actions"""
while self.running:
signal = self.signal # Dummy signal, set by external method, instead of received message from socket
if signal == 'end': # End signal received
self.master.destroy() # Destroy tkinter GUI, error occurs here
self.running = False # Terminate while loop
else:
time.sleep(0.2)
def send_signal_after(receiver: Client, delay: float = 2.0):
"""Send a signal to the client after short delay"""
time.sleep(delay)
receiver.signal = 'end'
if __name__ == '__main__':
root = tk.Tk()
client = Client(root)
threading.Thread(target=send_signal_after, args=(client,)).start()
root.mainloop()
if client.thread: # Check if thread is still running, if so, wait for termination
client.thread.join()
I am running this on MacOS 12.1, Python 3.10.
Is there any other way to terminate the application? I know, I could probably use sys.exit(), but I would like to do this in a cleaner way.
Thank you!
So, to understand how to do it, I made an example:
This is the first file (the main one) :
import tkinter as tk
from tkinter import *
import threading
import file2 as file2
def func(gui):
# just some code around here
# start up the program
root = Tk()
# pass the root in the __init__ function from file2
mainGui = file2.file2Class(root)
# Start the new thread
theThread = threading.Thread(target=func, args=([mainGui]))
theThread.daemon = True
theThread.start()
# loop command
tk.mainloop()
And this is file2 :
import tkinter as tk
from tkinter import *
class file2Class:
root = None
def __init__(self, initialRoot):
self.root = initialRoot
self.createUI();
def createUI(self):
# This is an exit button
tk.Button(
self.root,
text="Exit",
font = "Verdana 10 bold",
fg="red",
command=self.root.destroy, # <- this is the exit command
width=25,
height=2).grid(row=0,column=0)
So, the most important thing is to make sure that you pass root in the args of the thread.
I hope I helped you!

Tkinter program runs function but the window pops up after it ends

Ok so I'm pretty new to tkinter and I can not solve this problem that I am having.
When I run the program, it runs the function and after it ends the window with the photo pops up, but the loop does not start the program again.
import tkinter as tk
from tkinter import*
from PIL import ImageTk,Image
from tkinter import messagebox
import YuaChanMainFunc
import time
def on_click(event=None):
# `command=` calls function without argument
# `bind` calls function with one argument
print("Hey Yua!")
YuaChanMainFunc.statement="hey yua"
class Window(Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.pack()
master.title("Yua-chan AI")
self.img = ImageTk.PhotoImage(Image.open("YuaChanAI/Yua Chan Artwork/YuaChan2.png"))
MainLB = tk.Label(master, image=self.img)
MainLB.bind('<Button-1>', on_click)
MainLB.pack()
b = tk.Button(root, text="Close", command=root.destroy)
b.pack()
#YuaChanMainFunc.YuaChanAIMainFunc()
root = tk.Tk()
#instance of the class
app = Window(root)
root.resizable(0,0)
root.geometry("310x500+1600+510")
YuaChanMainFunc.YuaChanAIMainFunc()
#Runs the application until we close
root.mainloop()
From what I understand you want "YuaChanMainFunc.YuaChanAIMainFunc()"
to run in the background while UI runs the foreground. For that you can start the "YuaChanMainFunc.YuaChanAIMainFunc()" in a different thread and run UI in the main thread itself. Now you can make "YuaChanMainFunc.YuaChanAIMainFunc()" an infinite loop and it wont block root.mainloop(). Also keep in mind root.mainloop() is also an infinite loop. SO anything you write after that will not execute until you close the program.
import threading
backend_thread = threading.Thread(target=YuaChanMainFunc.YuaChanAIMainFunc, args=())
backend_thread.daemon = True #This will make sure backend thread closes when you close ui
backend_thread.start()
root.mainloop()

Thread won't stop until window is destroyed, window won't destroy unless thread is stopped

So I have a window which is controlled by a thread that runs in the background and changes the GUI when necessary, at some point this thread will be instructed to change window (involving destroying the window it is in and starting up another window), but this never happens because the thread won't stop executing until the window is changed.
Below is a simplified example:
class Window1:
def __init__(...):
self.Master = tk.Tk()
# some code
self.BackgroundUpdates = threading.Thread(target=self.ActiveWindow)
self.BackgroundUpdates.start()
def ActiveWindow(self):
# gets some instruction
if instruction == 'next window':
nextWindow(self)
def StartWindow(self):
self.Master.mainloop()
def KillWindow(self):
self.Master.destroy()
class Window2:
def __init__(...):
self.Master = tk.Tk()
# some code...
def StartWindow(self):
self.Master.mainloop()
def nextWindow(objectWindow):
objectWindow.KillWindow()
# when this function is called it never gets past the line above
nextWindow = Window2()
nextWindow.StartWindow()
application = Window1()
application.StartWindow()
Is there a way that I could rearrange the way I handle the thread so that I don't run into this problem?
a runnable example:
import tkinter as tk
import threading
class MainWindow:
def __init__(self):
self.Master = tk.Tk()
self.Frame = tk.Frame(self.Master, width=100, height=100)
self.Frame.pack()
self.Updates = threading.Thread(target=self.BackgroundUpdates)
self.Updates.start()
def BackgroundUpdates(self):
# imagine instructions to be a really long list with each element being a
# different instruction
instructions = ['instruction1', 'instruction2', 'next window']
while True:
if instructions[0] == 'next window':
ChangeWindow(self)
else:
instructions.remove(instructions[0])
def StartWindow(self):
self.Master.mainloop()
def KillWindow(self):
self.Master.destroy()
class SecondaryWindow:
def __init__(self):
self.Master = tk.Tk()
self.Frame = tk.Frame(self.Master, width=100, height=100)
self.Frame.pack()
def StartWindow(self):
self.Master.mainloop()
def KillWindow(self):
self.Master.destroy()
def ChangeWindow(oldObject):
oldObject.KillWindow()
# the line above will halt the program, since it has to wait on the thread to
# finish before the window can be destroyed, but this function is being called
# from within the thread and so the thread will never stop executing
del oldObject
newObject = SecondaryWindow()
newObject.StartWindow()
window = MainWindow()
window.StartWindow()
I realised that tkinter is singularly threaded, it can be explained more here:
https://stackoverflow.com/a/45803955/11702354
The problem was that I was trying to destroy my window from a different thread to the one it was created in. To solve this problem I had to use the 'after' method from the Tkinter module as well as using a event, this meant that I could control the background stuff (i.e. wait on a specific command from my connected server) and when I needed to change the window I would set the event.
Part of my adapted code can be seen below:
def CheckEvent(self):
if LOBBY_EVENT.is_set():
ChangeWindow(self, 'game')
self.Master.after(5000, self.CheckEvent)
def StartWindow(self):
self.Master.after(5000, self.CheckEvent)
self.Master.after(2000, self.HandleInstruction)
self.Master.mainloop()
So whenever I was calling the StartWindow method for my window, it would check whether the event has been set every 5 seconds, and then every 2 seconds it would go to a separate function 'HandleInstruction' which allowed me to create a response in my GUI (I also used queues to pass information to this function)
I hope this clears up confusion if anyone is to stumble across it!

Passing variables into a tkinter class window

I want to create a GUI in tkinter that not only executes commands when a button is pressed, but responds to the state of a larger script running in a separate thread.
I have really dug around and tried to find some information on message passing, and I have found some great info on the pickle module, using multiprocessing and its built in tools and also threading, and queuing. I have even dug into David Beazley's lesson on concurrency located here. I just can't get the syntax right on any of those methods.
I have broken down my code into a small functional unit that should launch a little tkinter window like this:
tkinter window
The code below has a "launchGUI" function that launches my tkinter GUI, a "myLoop" function that starts the threads and will also loop to drive my larger program later, right now it just rotates the blink variable. I also have a blinkCheck method in my class that checks the status of the blink variable in the class.
I don't know if I am even putting my message receiver in the right place. In the following example code I am just trying to pass a global variable into the class. I know it is getting into the class, because the blinkCheck() method works even though uncommenting that method crashes the window. However, with the method turned off the label in the GUI never changes. I think the window crashing is the least of my worries, it must be because i have another while loop running.
What is the correct way to get that number in Label to change?
Here is my example code:
import tkinter as tk
from tkinter import Frame, Label
import time
import threading
blink = 0
class MyClass(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
global blink
self.label = Label(master, text=blink)
self.label.pack()
#self.blinkCheck()
def blinkCheck(self):
global blink
while True:
print("blink in blinkCheck method is = {}".format(blink))
time.sleep(2.5)
def launchGUI():
root = tk.Tk()
root.title("My Blinker")
app1 = MyClass(root)
app1.mainloop()
def myLoop():
global blink
t1=threading.Thread(target=launchGUI)
t1.daemon = True
t1.start()
print("blink in blinker function is {}".format(blink))
while True:
if blink == 0:
blink = 1
else:
if blink == 1:
blink = 0
time.sleep(2.5)
if __name__=="__main__":
myLoop()
In your description you have mentioned something about involving buttons. I do not see that in your provided snippet. But with buttons it is possible to configure the label, i.e:
from tkinter import Label, Button
blink = 0
class MyClass(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
global blink
self.label = Label(master, text=blink)
self.button = Button(master, text="Button", command=lambda: foo(self.label))
self.label.pack()
self.button.pack()
#self.blinkCheck()
def blinkCheck(self):
global blink
while True:
print("blink in blinkCheck method is = {}".format(blink))
time.sleep(2.5)
def foo(self, label):
label.config(text=blink)
Conventionally, this would be the most simple way to configure a label within an active thread.
If anyone feels like this answer may not be fully correct, please do edit it because I am new to Stack Overflow!
First, the GUI must run in main thread, and must not blocked by a infinite loop. Use after instead. To communicate, use some appropriate object from threading, e.g. Event:
import tkinter as tk
import time
import threading
class MyClass(tk.Frame):
def __init__(self, master, event):
super().__init__(master)
self.master = master
self.event = event
self.label = tk.Label(master, text='')
self.label.pack()
self.after(100, self.blink_check)
def blink_check(self):
self.label['text'] = self.event.is_set()
self.after(100, self.blink_check)
def blink(event):
while True:
event.set()
time.sleep(2.5)
event.clear()
time.sleep(2.5)
def main():
root = tk.Tk()
root.title("My Blinker")
event = threading.Event()
t = threading.Thread(target=blink, args=(event,))
t.daemon = True
t.start()
frame = MyClass(root, event)
root.mainloop()
if __name__=="__main__":
main()

Categories