I am fairly new to Python and this is my first project using tkinter. I have my entire project working fine with one exception. I have built the tkinter code into a class and it all works but I cannot figure out how to call the methods from outside of the class.
When I create the object on the following line in main, I get NameError: name 'robotGUI' is not defined
class botGUI:
def __init__(self):
#Init Code
def updateStatus(self):
#Code Here
robotGUI = botGUI()
If I initialize the variable "robotGUI" to None, the code runs but when I later try to access one of its methods I get AttributeError: 'NoneType' object has no attribute 'doSomething'. It appears that the robotGUI object is not being created but I do not understand why.
I have searched everywhere and have found some close answers but nothing that exactly pertains to this issue. I have a handfull of other classes that are working perfect in this program so I am sure it has to do with tkinter and its internal mainloop just havent been able to pin point it.
Here is my greatly reduced and simplified code showing the problem:
#!/usr/bin/env python3
#Imports
import socket, select, errno, sys, queue, time, threading, cv2
from tkinter import *
from tkinter import font
from PIL import Image, ImageTk
#GUI
class botGUI:
def __init__(self):
#Create the Window Object and Setup the Window
self.window = Tk()
self.window.geometry("800x480+0+0")
self.window.overrideredirect(True)
self.window.fullScreenState = False
#Code to Generate Gaphics Here .....
#Call Repeating Status Update Script and Start the Main Loop
self.updateStatus()
self.window.mainloop()
def updateStatus(self):
#Code to Handle Updating Screen Objects Here ....
print("Update Status Running")
#Set this function to be called again
self.window.after(1000, lambda: self.updateStatus())
def doSomething(self, myStr):
#Code to change something on the screen ...
print(f"Command: {str(myStr)}")
def doSomethingElse(self, myStr):
#Code to change something on the screen ...
print(f"Command: {str(myStr)}")
#Main Task - Since tKinter is running in the main loop, all of the main loop code is moved to here
def main_loop():
global robotGUI
robotDataReceived = True #This is only for this posting
#Main Loop
while True:
#If Incoming Data from Robot, Get and Process It!
if robotDataReceived:
robotCmdHandler()
#Anti Blocking Delay (Much shorter, set higher for this post)
time.sleep(2)
#Robot Command Handler
def robotCmdHandler():
global robotGUI
#Code to get a command string and process it goes here .....
cmd = "dosomething" #Temporary for this post
#Handle command
if (cmd == "dosomething"):
print("Processing Command")
robotGUI.doSomething("Do This")
if __name__ == '__main__':
global robotGUI
robotGUI = None
#Create and Start Threads
t1 = threading.Thread(target=main_loop, name='t1')
t1.start()
#Create GUI Object
robotGUI = botGUI()
#Wait until threads are finished
t1.join()
Remove the call self.window.mainloop() from botGUI.__init__(), then you can:
create the instance of botGUI: robotGUI = botGUI()
create the thread and start it
call roboGUI.window.mainloop()
Below is the modified code:
#!/usr/bin/env python3
#Imports
import socket, select, errno, sys, queue, time, threading, cv2
from tkinter import *
from tkinter import font
from PIL import Image, ImageTk
#GUI
class botGUI:
def __init__(self):
#Create the Window Object and Setup the Window
self.window = Tk()
self.window.geometry("800x480+0+0")
self.window.overrideredirect(True)
self.window.fullScreenState = False
#Code to Generate Gaphics Here .....
#Call Repeating Status Update Script and Start the Main Loop
self.updateStatus()
#self.window.mainloop()
def updateStatus(self):
#Code to Handle Updating Screen Objects Here ....
print("Update Status Running")
#Set this function to be called again
self.window.after(1000, lambda: self.updateStatus())
def doSomething(self, myStr):
#Code to change something on the screen ...
print(f"Command: {str(myStr)}")
def doSomethingElse(self, myStr):
#Code to change something on the screen ...
print(f"Command: {str(myStr)}")
#Main Task - Since tKinter is running in the main loop, all of the main loop code is moved to here
def main_loop():
#global robotGUI
robotDataReceived = True #This is only for this posting
#Main Loop
while True:
#If Incoming Data from Robot, Get and Process It!
if robotDataReceived:
robotCmdHandler()
#Anti Blocking Delay (Much shorter, set higher for this post)
time.sleep(2)
#Robot Command Handler
def robotCmdHandler():
#global robotGUI
#Code to get a command string and process it goes here .....
cmd = "dosomething" #Temporary for this post
#Handle command
if (cmd == "dosomething"):
print("Processing Command")
robotGUI.doSomething("Do This")
if __name__ == '__main__':
#Create GUI Object
robotGUI = botGUI()
#Create and Start Threads
t1 = threading.Thread(target=main_loop, name='t1')
t1.start()
# start the GUI main loop
robotGUI.window.mainloop()
#Wait until threads are finished
t1.join()
You must define robotGUI outside all of the functions like that:
robotGUI = None
def main_loop():
global robotGUI
robotDataReceived = True #This is only for this posting
#Main Loop
while True:
#If Incoming Data from Robot, Get and Process It!
if robotDataReceived:
robotCmdHandler()
#Anti Blocking Delay (Much shorter, set higher for this post)
time.sleep(2)
Related
I'm trying to make a python3 application for my Raspberry Pi 4B and I have the tkinter windows working fine, but need to add asynchronous handling to allow tkinter widgets to respond while processing asynchronous actions initiated by the window's widgets.
The test code is using asyncio and tkinter. However, without root.mainloop(), since asyncio loop.run_forever() is called at the end instead. The idea is that when the user clicks the main window's close box, RequestQuit() gets called to set the quitRequested flag and then when control returns to the event loop, root.after_idle(AfterIdle) would cause AfterIdle to be called, where the flag is checked and if true, the event loop is stopped, or that failing, the app is killed with exit(0).
The loop WM_DELETE_WINDOW protocol coroutine RequestQuit is somehow not getting called when the user clicks the main window close box, so the AfterIdle coroutine never gets the flag to quit and I have to kill the app by quitting XQuartz.
I'm using ssh via Terminal on MacOS X Big Sur 11.5.2, connected to a Raspberry Pi 4B with Python 3.7.3.
What have I missed here?
(I haven't included the widgets or their handlers or the asynchronous processing here, for brevity, since they aren't part of the problem at hand.)
from tkinter import *
from tkinter import messagebox
import aiotkinter
import asyncio
afterIdleProcessingIntervalMsec = 500 # Adjust for UI responsiveness here.
busyProcessing = False
quitRequested = False
def RequestQuit():
global quitRequested
global busyProcessing
if busyProcessing:
answer = messagebox.askquestion('Exit application', 'Do you really want to abort the ongoing processing?', icon='warning')
if answer == 'yes':
quitRequested = True
def AfterIdle():
global quitRequested
global loop
global root
if not quitRequested:
root.after(afterIdleProcessingIntervalMsec, AfterIdle)
else:
print("Destroying GUI at: ", time.time())
try:
loop.stop()
root.destroy()
except:
exit(0)
if __name__ == '__main__':
global root
global loop
asyncio.set_event_loop_policy(aiotkinter.TkinterEventLoopPolicy())
loop = asyncio.get_event_loop()
root = Tk()
root.protocol("WM_DELETE_WINDOW", RequestQuit)
root.after_idle(AfterIdle)
# Create and pack widgets here.
loop.run_forever()
The reason why your program doesn't work is that there is no Tk event loop, or its equivalent. Without it, Tk will not process events; no Tk callback functions will run. So your program doesn't respond to the WM_DELETE_WINDOW event, or any other.
Fortunately Tk can be used to perform the equivalent of an event loop as an asyncio.Task, and it's not even difficult. The basic concept is to write a function like this, where "w" is any tk widget:
async def new_tk_loop():
while some_boolean:
w.update()
await asyncio.sleep(sleep_interval_in_seconds)
This function should be created as an asyncio.Task when you are ready to start processing tk events, and should continue to run until you are ready to stop doing that.
Here is a class, TkPod, that I use as the basic foundation of any Tk + asyncio program. There is also a trivial little demo program, illustrating how to close the Tk loop from another Task. If you click the "X" before 5 seconds pass, the program will close immediately by exiting the mainloop function. After 5 seconds the program will close by cancelling the mainloop task.
I use a default sleep interval of 0.05 seconds, which seems to work pretty well.
When exiting such a program there are a few things to think about.
When you click on the "X" button on the main window, the object sets its app_closing variable to false. If you need to do some other clean-up, you can subclass Tk and over-ride the method close_app.
Exiting the mainloop doesn't call the destroy function. If you need to do that, you must do it separately. The class is a context manager, so you can make sure that destroy is called using a with block.
Like any asyncio Task, mainloop can be cancelled. If you do that, you need to catch that exception to avoid a traceback.
#! python3.8
import asyncio
import tkinter as tk
class TkPod(tk.Tk):
def __init__(self, sleep_interval=0.05):
self.sleep_interval = sleep_interval
self.app_closing = False
self.loop = asyncio.get_event_loop()
super().__init__()
self.protocol("WM_DELETE_WINDOW", self.close_app)
# Globally suppress the Tk menu tear-off feature
# In the following line, "*tearOff" works as documented
# while "*tearoff" does not.
self.option_add("*tearOff", 0)
def __enter__(self):
return self
def __exit__(self, *_x):
self.destroy()
def close_app(self):
self.app_closing = True
# I don't know what the argument n is for.
# I include it here because pylint complains otherwise.
async def mainloop(self, _n=0):
while not self.app_closing:
self.update()
await asyncio.sleep(self.sleep_interval)
async def main():
async def die_in5s(t):
await asyncio.sleep(5.0)
t.cancel()
print("It's over...")
with TkPod() as root:
label = tk.Label(root, text="Hello")
label.grid()
t = asyncio.create_task(root.mainloop())
asyncio.create_task(die_in5s(t))
try:
await t
except asyncio.CancelledError:
pass
if __name__ == "__main__":
asyncio.run(main())
I have a problem (Python just hang) if I enable the line "thread.join()" after redirected output to tkinter.
Here is the code:
import tkinter as tk
import sys
import threading
def run():
thread = threading.Thread(target=test)
thread.start()
# thread.join()
print('Want to add some code here')
def test():
print('Process thread...')
class Redirect():
def __init__(self, widget):
self.widget = widget
def write(self, text):
self.widget.insert('end', text)
#def flush(self):
# pass
root = tk.Tk()
text = tk.Text(root)
text.pack()
button = tk.Button(root, text='TEST', command=run)
button.pack()
old_stdout = sys.stdout
sys.stdout = Redirect(text)
root.mainloop()
sys.stdout = old_stdout
Current it prints out:
Want to add some code here
Process thread...
And what I expected (when using thread.join(after finished the thread):
Process thread...
Want to add some code here
If I don't use Redirect function, then the case work well, so I guess I need to add something into the class Redirect above.
Can you please help to take a look and give yur comment?
Thanks
If you join the thread, tkinter will be held up until the thread is finished, but you can't start the thread either because it won't give the desired output. The solution is to use a global variable to keep track of whether the thread has finished or not, then check it periodically with root.after.
def run():
global completed
thread = threading.Thread(target=test)
thread.start()
completed = False
wait_for_finish()
def wait_for_finish():
global completed
if completed:
print('Want to add some code here')
else:
root.after(100, wait_for_finish)
def test():
global completed
print('Process thread...')
completed = True
This calls wait_for_finish every 100ms to check if completed is True. When it is, it runs the next part of the code.
I'm doing Tkinter. My functions run correctly (playing a bell sound once a minute), but it's not running on a thread. Whenever I click the Start button, the program window grays out and the top says "Not responding" (because my recursive calls to start() are locking it, I'm assuming.)
Why am I not threading correctly? Thanks.
def start():
now = tt1.time()
listOfTimes = []
for t in timeSlotEFs:
listOfTimes.append(t.get())
now = datetime.now()
timee = now.strftime('%H%M')
print('here')
for t in listOfTimes:
if t==timee:
winsound.PlaySound('newhourlychimebeg.wav',winsound.SND_FILENAME)
s.enterabs(now+60,1,start) #I want to call recursively every 60 seconds
s.run()
def start2():
t = threading.Thread(target=start)
t.run()
startBtn = Button(ssFrame, text='Start', command=start2)
startBtn.grid(row=0,column=0,padx=paddX,pady=paddY)
It feels like you mixed between the definitions that threading.Thread imports from.
You should create run function and then start the thread.
This way i see the result and it works:
from tkinter import *
import threading
root = Tk()
def run():
print('hello Thread')
def start2():
t = threading.Thread(target=run)
t.start()
print(t) # Output: <Thread(Thread-1, stopped 10580)>
startBtn = Button(root, text='Start', command=start2)
startBtn.grid(row=0,column=0)
root.mainloop()
In start2(), I changed t.run() to t.start(). That was it, and it now works. Thx Jim.
I'm writing a program with a GUI using TKinter, in which the user can click a button and a new process is started to perform work using multiprocess.Process. This is necessary so the GUI can still be used while the work is being done, which can take several seconds.
The GUI also has a text box where the status of the program is displayed when things happen. This is often straight forward, with each function calling an add_text() function which just prints text in the text box. However, when add_text() is called in the separate process, the text does not end up in the text box.
I've thought about using a Pipe or Queue, but that would require using some sort of loop to check if anything has been returned from the process and that would also cause the main (GUI) process to be unusable. Is there some way to call a function in one process that will do work in another?
Here's an simple example of what I'm trying to do
import time
import multiprocessing as mp
import tkinter as tk
textbox = tk.Text()
def add_text(text):
# Insert text into textbox
textbox.insert(tk.END, text)
def worker():
x = 0
while x < 10:
add_text('Sleeping for {0} seconds'.format(x)
x += 1
time.sleep(1)
proc = mp.Process(target=worker)
# Usually happens on a button click
proc.start()
# GUI should still be usable here
The asyncronous things actually require loop.
You could attach function to the TkInter's loop by using Tk.after() method.
import Tkinter as tk
class App():
def __init__(self):
self.root = tk.Tk()
self.check_processes()
self.root.mainloop()
def check_processes(self):
if process_finished:
do_something()
else:
do_something_else()
self.after(1000, check_processes)
app=App()
I ended up using a multiprocessing.Pipe by using TKinter's after() method to perform the looping. It loops on an interval and checks the pipe to see if there's any messages from the thread, and if so it inserts them into the text box.
import tkinter
import multiprocessing
def do_something(child_conn):
while True:
child_conn.send('Status text\n')
class Window:
def __init__(self):
self.root = tkinter.Tk()
self.textbox = tkinter.Text()
self.parent_conn, child_conn = multiprocessing.Pipe()
self.process = multiprocessing.Process(target=do_something, args=(child_conn,))
def start(self):
self.get_status_updates()
self.process.start()
self.root.mainloop()
def get_status_updates()
status = self.check_pipe()
if status:
self.textbox.add_text(status)
self.root.after(500, self.get_status_updates) # loop every 500ms
def check_pipe():
if self.parent_conn.poll():
status = self.parent_conn.recv()
return status
return None
I have a tkinter gui that has a button that starts a process. During this process there is an if statement, if this statement is true then then the process ends. When the process ends I want the GUI to be kept open and not show an error. I've tried os._exit() but it closes the gui as well.
from Tkinter import *
import tkMessageBox
def Program():
#Process
#Process
if #something happens#:
#Stop process but keep gui open and dont show errors
root = Tk()
root.title("GUI")
root.geometry('450x300+200+200')
labelText=StringVar()
labelText.set("Program")
label1=Label(root,textvariable=labelText,height=4)
label1.pack()
mbutton=Button(text='Start Program',command=Model).pack()
root.mainloop()
You could run GUI in the main thread and put the part that should terminate independently in a background thread. Add try/except in the thread to suppress traceback e.g.:
import threading
def bgthread(gui_ready, result_queue):
gui_ready.wait()
while True:
try:
# do some work ...
result_queue.put(result) # GUI gets results e.g.,
# via q.get_nowait() in a
# widget.after() callback
if something_happened():
break # exit
except: #NOTE: don't use bare except unless it is absolutely necessary
logger.error() # log to file
break # exit
# setup logging
# ...
ready = threading.Event()
q = Queue.Queue()
threading.Thread(target=bgthread, args=(ready,q)).start()
# setup gui here
...
root.mainloop() # call ready.set() in some GUI code then it is ready
Python code worked using geo_pythoncl suggestion of using return.
from Tkinter import *
import tkMessageBox
def Program():
#Process
#Process
if #something happens#:
#Stop process but keep gui open and dont show errors
return
root = Tk()
root.title("GUI")
root.geometry('450x300+200+200')
labelText=StringVar()
labelText.set("Program")
label1=Label(root,textvariable=labelText,height=4)
label1.pack()
mbutton=Button(text='Start Program',command=Model).pack()
root.mainloop()