Multithreading: tkinter mainloop not in main thread - python

I was wondering if there was a way to run the tkinter mainloop on a separate thread (not the mainthread) so that the terminal is "free".
Let's say we have a simple GUI:
import tkinter as tk
class Application(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.grid()
self.createWidgets()
def printHello(self):
print("Hello")
def createWidgets(self):
self.quitButton = tk.Button(self, text='Quit',
command=self.quit)
self.quitButton.grid(row=1,column=0)
self.printButton = tk.Button(self, text='Print',command=lambda: self.printHello())
self.printButton.grid(row=1,column=1)
app = Application()
app.master.title('Sample application')
app.mainloop()
Observed behavior: if I run this file using the terminal in PyCharm (let's say) using: python application.py, the terminal is then "frozen"/"occupied" by the tkinter mainloop and I am not able to type anything in the terminal.
Desired behavior: if I run the gui file with python application.py, the terminal is "free" such that it is able to take in additional commands.
I'm aware that python application.py & is supposed to free up the terminal but that hasn't worked for me. I'm not very experienced with threading or GUI processes but is there a way to run the tkinter mainloop in a different process / thread so that the terminal will be "free"?

You can use the threading module to run the GUI in a background thread while using the terminal in the main thread.
Try this code. It will start the GUI then allow input from the terminal.
import tkinter as tk
import threading
class Application(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.grid()
self.createWidgets()
def printHello(self):
print("Hello")
def createWidgets(self):
self.quitButton = tk.Button(self, text='Quit',
command=self.quit) # exits background (gui) thread
self.quitButton.grid(row=1,column=0)
self.printButton = tk.Button(self, text='Print',command=lambda: self.printHello())
self.printButton.grid(row=1,column=1)
def runtk(): # runs in background thread
app = Application()
app.master.title('Sample application')
app.mainloop()
thd = threading.Thread(target=runtk) # gui thread
thd.daemon = True # background thread will exit if main thread exits
thd.start() # start tk loop
while True: # run in main thread
x = input("Enter a value or Q to quit: ")
if x.lower() == 'q':
exit()
print('You entered', x)

Related

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()

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

I have created a tkinter GUI in the following structure:
import tkinter as tk
import threading
class App:
def __init__(self, master):
self.display_button_entry(master)
def setup_window(self, master):
self.f = tk.Frame(master, height=480, width=640, padx=10, pady=12)
self.f.pack_propagate(0)
def display_button_entry(self, master):
self.setup_window(master)
v = tk.StringVar()
self.e = tk.Entry(self.f, textvariable=v)
buttonA = tk.Button(self.f, text="Cancel", command=self.cancelbutton)
buttonB = tk.Button(self.f, text="OK", command=threading.Thread(target=self.okbutton).start)
self.e.pack()
buttonA.pack()
buttonB.pack()
self.f.pack()
def cancelbutton(self):
print(self.e.get())
self.f.destroy()
def okbutton(self):
print(self.e.get())
def main():
root = tk.Tk()
root.title('ButtonEntryCombo')
root.resizable(width=tk.NO, height=tk.NO)
app = App(root)
root.mainloop()
main()
I want to prevent the GUI from freezing when running a function (in the example code it's the function of the ok-button). For that I found the solution of using the thread-module as best practice. But the problem is that when I want to run the code once again, python returns this traceback:
RuntimeError: threads can only be started once
I'm totally aware of the problem that threads can be only starten once as stated in the error message. My question is: How can I stop a thread to start it a second time or does anybody has a better workaround for preventing the GUI from freezing and pressing a button/running a function multiple times?
BR and thank you
Lorenz
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.

Why does closing a tkinter child window with `frame.quit` exit my application? [duplicate]

This question already has answers here:
How do I close a tkinter window?
(17 answers)
Closed 4 years ago.
I have a tkinter application with two windows: A MainWindow that is created on startup, and a ChildWindow which is opened after clicking a button.
The ChildWindow should close itself if the user presses a button. However, when I try calling frame.quit, it terminates the entire application instead.
import tkinter as tk
class ChildWindow:
def __init__(self, master):
self.top = tk.Toplevel(master)
self.frame = tk.Frame(self.top)
self.frame.pack()
# BUG: Clicking "Close" will fully exit application
self.close_button = tk.Button(self.frame, text="Close", command=self.frame.quit)
self.close_button.pack()
class MainWindow:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(self.master)
self.about_button = tk.Button(self.frame, text="Open child window", command=self._open_child_window)
self.about_button.pack()
self.frame.pack()
def _open_child_window(self):
self.about_window = ChildWindow(self.master)
root = tk.Tk()
lf = MainWindow(root)
root.mainloop()
Screenshots:
Why does frame.quit exit my application? How can I close the child window without exiting?
It is because quit causes mainloop to exit. With no event loop running, there's nothing left to keep the main window alive.
If you want to close a child window, call the destroy method.
self.close_button = tk.Button(..., command=self.top.destroy)

Threads and tkinter

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.

Python Tkinter - closing a child window with an exit button

I have a Raspberry Pi with the Piface adaptor board. I have made a GUI which controls the LED's on the Piface board.
One button on the GUI opens a new window which, on the press of a button, starts and stops running a small piece of code to make the LED's run up and down continuously, like Knight Riders car, using a While loop in a thread.
In this new window I have added a EXIT button. I want to add a piece of code that will close the new window when I click the EXIT button, and then return to the main window.
I have looked up many examples but just can't quite see what I should put or where. I have tried the 'quit' but it closed the whole program.
Having looked at many examples I maybe creating my new window in not quite the right way so feel free to tell me if there are better ways.
So is there a better way of doing it? Any pointers would be appreciated.
Thanks in advance.
Heres a piece of the code....
def new_window(self):
print('New Window')
self.newWindow = tk.Toplevel(self.master)
self.app = App2(self.newWindow)
self.newWindow.grab_set() # I added this line to stop opening multiple new windows
class App2:
def __init__(self, master):
frame = Frame(master)
frame.pack()
Label(frame, text='Turn LED ON').grid(row=0, column=0)
Label(frame, text='Turn LED OFF').grid(row=0, column=1)
self.button0 = Button(frame, text='Knight Rider OFF', command=self.convert0)
self.button0.grid(row=2, column=0)
self.LED0 = Label(frame, image=logo2)
self.LED0.grid(row=2, column=1)
self.button9 = Button(frame, text='Exit', command=self.close_window)
self.button9.grid(row=3, column=0)
def convert0(self, tog=[0]):
tog[0] = not tog[0]
if tog[0]:
print('Knight Rider ON')
self.button0.config(text='Knight Rider ON')
t=threading.Thread(target=self.LED)
t.start()
self.signal = True #added to stop thread
self.LED0.config(image = logo)
else:
print('Knight Rider OFF')
self.button0.config(text='Knight Rider OFF')
self.signal = False #added to stop thread
self.LED0.config(image = logo2)
def LED(self):
while self.signal: #added to stop thread
a=0
while self.signal: #added to stop thread
pfio.digital_write(a,1) #turn on
sleep(0.05)
pfio.digital_write(a,0) #turn off
sleep(0.05)
a=a+1
if a==7:
break
while self.signal: #added to stop thread
pfio.digital_write(a,1) #turn on
sleep(0.05)
pfio.digital_write(a,0) #turn off
sleep(0.05)
a=a-1
if a==0:
break
def close_window(self):
print('Close Child window')
#self.newWindow.destroy() Not sure what to put here?
If you put the new_window into your App2 then you should be fine.
self.newWindow.destroy()
Destroys the window. This is the right call. The window is closed then and all widgets in the window also get destroyed.
quit() will stop the mainloop() In this case the the program ends at the last line and also destroys everything.
You definitely want to use destroy.
class App2:
newWindow = None
def close_window(self):
print('Close Child window')
if self.newWindow:
try: self.newWindow.destroy()
except (): pass # fill in the error here
self.newWindow = None
def new_window(self):
print('New Window')
self.close_window()
self.newWindow = tk.Toplevel(self.master)
self.app = App2(self.newWindow)
self.newWindow.grab_set()
#classmethod
def start_app(cls):
window = tk.Tk(self.master)
app = App2(window)
return app
You should not acces Tkinter from threads. Have a look at alternatives
I was confused by quit and destroy, too, when I was starting with Tkinter.
In Tk windows are destroyed using the destroy method. So if you have a dialog toplevel and you want to get rid of it you call its destroy() method. Or you can withdraw it in which case the object continues to exist but is no longer on-screen and to re-show it you deiconify() the toplevel frame. It's more common to destroy them though. Here is a simple example creating and destroying a child dialog:
import sys
from Tkinter import *
class App(Frame):
def __init__(self, parent = None):
Frame.__init__(self, parent)
self.grid()
self.button = Button(self, text = "Create Dialog", command=self.CreateDialog)
self.button.grid()
def CreateDialog(self):
dialog = Toplevel(self)
dialog.wm_title("Dialog window")
dialog.wm_transient(self)
dialog.wm_protocol("WM_DELETE_WINDOW", lambda: self.onDeleteChild(dialog))
button = Button(dialog, text="Close", command=lambda: self.onDeleteChild(dialog))
button.grid()
def onDeleteChild(self, w):
w.destroy()
def main():
app = App()
app.mainloop()
if __name__ == "__main__":
sys.exit(main())
You should also consider looking at using a timer in the code to drive the LED loop rather than a while loop. Take a look at this answer using Tk's after function to run code after an interval. If you re-schedule another after call in the handler, then you can arrange a function to be run at regular intervals and avoid blocking the event handling without requiring additional threads.

Categories