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()
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 plan to make a simple test with tkinter which will change the left text with new input.
But it seems threading is not easy as my thought. It still be hang after 2 times of input.
from tkinter import *
import threading
win = Tk()
label1 = Label(win, text="this is a test on the left")
label1.pack(side=LEFT)
label2 = Label(win, text="this is a test on the right")
label2.pack(side=RIGHT)
def set_text():
while(True):
content=input("let enter the substuition:")
label1.config(text = content)
win.after(100, set_text)
setTextthr=threading.Thread(target = set_text)
setTextthr.start()
win.mainloop()
It is very impressed if you can point out why it happened and how to fix.
Thanks
There are some unwritten rules about threading in combination with tkinter.
One of them is not to touch any widget outside of the thread of tkinter.
This line violates this rule.
label1.config(text = content)
In order to do this you can use a tkinter.StingVar which isn't directly in the main event loop of tkinter.
The call from inside the function to itself with after some time will create a new stack of a while-loops on top. Below you find a working example which has its limits. For another approach you can take a look at this or this.
from tkinter import *
import threading
win = Tk()
var = StringVar(value="this is a test on the left")
label1 = Label(win,textvariable=var)
label1.pack(side=LEFT)
label2 = Label(win, text="this is a test on the right")
label2.pack(side=RIGHT)
def set_text():
while(True):
content=input("let enter the substuition:")
var.set(content)
setTextthr=threading.Thread(target = set_text)
setTextthr.start()
win.mainloop()
The usual way to approach getting data from threads in an event-driven program (as it is with tkinter) is to use some update loop (.after) to schedule a check on the data container (queue.Queue or a simple list) that contains data from the thread. Then additionally check if the thread is alive at all, and if it is not you can stop the update loop because there is nothing to update anymore. Also use some flag (threading.Event) to stop the thread loop upon destroying the window. The only issue is that input is used which makes it so that at the end at least the Enter key has to be pressed.
The code example (explanation in code comments):
import tkinter as tk
import threading
import queue
# this is the function that will run in another thread
def ask_input(q): # argument is the queue
# here check if the event is set, if it is
# don't loop anymore, however it will still wait
# for input so that has to be entered and
# then the thread will stop
while not event.is_set():
user_input = input('New content: ')
# put the user entered data into the queue
q.put(user_input)
# the function that will update the label
def update_label(q, t): # pass in the the queue and thread
# this loop simply gets the last item in the queue
# otherwise (not in this case perhaps) it may be a little
# too slow in updating since it only runs every 100 ms
# so it need to catch up
while not q.empty():
# get data from the queue
text = q.get()
# here it is save to update the label since it is the same
# thread where all of the tkinter runs
label.config(text=text)
# check if thread still runs (in this case this specific check
# is unnecessary since the thread is stopped only after
# the main window is closed)
if t.is_alive():
# schedule the next call with the same queue and thread
root.after(100, update_label, q, t)
# the function that initiates the thread
def start_thread():
# create a queue to pass as the argument to the thread and updater
_queue = queue.Queue()
# create and start a thread
thread = threading.Thread(target=ask_input, args=(_queue,))
thread.start()
# start updating the label
update_label(_queue, thread)
# check if the code is run without importing
if __name__ == '__main__':
root = tk.Tk()
# set this attribute so that the window always stays on top
# so that you can immediately see the changes in the label
root.attributes('-topmost', True)
# create the event
event = threading.Event()
# if window is destroyed set event which will break the loop
root.bind('<Destroy>', lambda _: event.set())
label = tk.Label(root)
label.pack()
start_thread()
root.mainloop()
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 encounter a problem that slows me down a lot in my development ..
Indeed, I can not update the Queue with the Threading module of python!
I have searched several sites, and I could not find fault that could prevent my variable from updating.
My tkinter button should allow me to run another python script. To do this, I use Threading so I can use the GUI without interrupting it.
I explain my problem:
My tkinter button should allow me to run another python script. To do this, I use Threading so I can use the GUI without it being interrupted. Another button should allow me to update the Queue, and this is what the action does not do.
My main script with Tkinter:
import Tkinter, cv2
from Tkinter import *
from threading import Thread
import threading, Queue
import pyautogui, os, time, urllib2, urlparse
import cv2
from yes2 import *
def print1():
global kill, q
kill = []
q = Queue.Queue()
q.put("True")
thread = Thread(target = main, args=(kill, q))
thread.start()
def stop():
global q
q.put("False")
print q.get()
root = Tkinter.Tk()
root.title('breakable')
bouton= Button(root, text="Run", command=print1)
bouton.grid(row=3, column=0)
bouton= Button(root, text="stop", command=stop)
bouton.grid(row=3, column=1)
root.mainloop()
I want to open this other script:
def main(kill, q):
while True:
try:
get = q.get(timeout=2)
print get
except Empty as error:
print("Error too many times")
The value that comes out is "True", but when I click on my stop button, which is supposed to update my Queue in "False", well it does nothing
Thanks in advance :)
It does update your Queue, but on the very next line you get the value out again. Remove the print q.get() from the "stop" function.
Ctrl-C/SIGTERM/SIGINT seem to be ignored by tkinter. Normally it can be captured again with a callback. This doesn't seem to be working, so I thought I'd run tkinter in another thread since its mainloop() is an infinite loop and blocks. I actually also want to do this to read from stdin in a separate thread. Even after this, Ctrl-C is still not processed until I close the window. Here's my MWE:
#! /usr/bin/env python
import Tkinter as tk
import threading
import signal
import sys
class MyTkApp(threading.Thread):
def run(self):
self.root = tk.Tk()
self.root.mainloop()
app = MyTkApp()
app.start()
def signal_handler(signal, frame):
sys.stderr.write("Exiting...\n")
# think only one of these is needed, not sure
app.root.destroy()
app.root.quit()
signal.signal(signal.SIGINT, signal_handler)
Results:
Run the app
Ctrl-C in the terminal (nothing happens)
Close the window
"Exiting..." is printed and I get an error about the loop already having exited.
What's going on here and how can I make Ctrl-C from the terminal close the app?
Update: Adding a poll, as suggested, works in the main thread but does not help when started in another thread...
class MyTkApp(threading.Thread):
def poll(self):
sys.stderr.write("poll\n")
self.root.after(50, self.poll)
def run(self):
self.root = tk.Tk()
self.root.after(50, self.poll)
self.root.mainloop()
here's a working example that catches control c in the windows or from the command line. this was tested with 3.7.2 this seems simpler than the other solutions. I almost feel like I'm missing something.
import tkinter as TK
import signal
def hello():
print("Hello")
root = TK.Tk()
TK.Button(root, text="Hi", command=(hello)).pack( )
def handler(event):
root.destroy()
print('caught ^C')
def check():
root.after(500, check) # time in ms.
# the or is a hack just because I've shoving all this in a lambda. setup before calling main loop
signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))
# this let's the terminal ^C get sampled every so often
root.after(500, check) # time in ms.
root.bind_all('<Control-c>', handler)
root.mainloop()
Since your tkinter app is running in another thread, you do not need to set up the signal handler in the main thread and just use the following code block after the app.start() statement:
import time
while app.is_alive():
try:
time.sleep(0.5)
except KeyboardInterrupt:
app.root.destroy()
break
You can then use Ctrl-C to raise the KeyboardInterrupt exception to close the tkinter app and break the while loop. The while loop will also be terminated if you close your tkinter app.
Note that the above code is working only in Python 2 (as you use Tkinter in your code).
Proper CTRL-C & SIGINT Usage in Python
The problem is that you are exiting the main thread, so the signal handler is basically useless. You need to keep it running, in a while loop, or my personal preference, Events from threading module. You can also just catch the KeyboardInterrupt exception generated by the CTRL-C event, rather than dealing with signal handlers.
SIGINT in Tkinter
Using tkinter, you must have the tkinter app run in a separate thread, so that it doesn't interfere with the signal handler or KeyboardInterrupt exception. In the handler, to exit, you need to destroy then update tkinter root. Update allows the tkinter to update so that it closes, without waiting for mainloop. Otherwise, user has to click on the active window to activate mainloop.
# Python 3
from tkinter import *
from threading import Thread
import signal
class MyTkApp(Thread):
def run(self):
self.root = Tk()
self.root.mainloop()
def sigint_handler(sig, frame):
app.root.quit()
app.root.update()
app = MyTkApp()
# Set signal before starting
signal.signal(signal.SIGINT, sigint_handler)
app.start()
Note: SIGINTs can also be caught if you set handler in same thread as tkinter mainloop, but you need to make tkinter window active after the signal so that it's mainloop can run. There is no way around this unless you run in new thread.
More Information on Tkinter & Command Line Communication
For more on communicating between tkinter and the command line, see Using Tkinter Without Mainloop. Basically, you can use update method in your loop, and then communicate with other threads and processes, etc. I would personally NOT recommend this, as you are essentially doing the job of the python thread control system, which is probably opposite of what you want to do. (python has a process that runs all internal threads in one external thread, so you are not taking advantage of multitheading, unless using multiprocessing module)
# Python 2
from Tkinter import *
ROOT = Tk()
LABEL = Label(ROOT, text="Hello, world!")
LABEL.pack()
LOOP_ACTIVE = True
while LOOP_ACTIVE:
ROOT.update()
USER_INPUT = raw_input("Give me your command! Just type \"exit\" to close: ")
if USER_INPUT == "exit":
ROOT.quit()
LOOP_ACTIVE = False
else:
LABEL = Label(ROOT, text=USER_INPUT)
LABEL.pack()