I have an app that tries to generate text when a button is pressed. Most of the time the text generation is fast but there is one function that takes about 20 seconds to execute (depending on the amount of text). During that process the GUI used to freeze so I transferred that function on a separate thread and everything is fine on that field.
Now I have an issue with the buttons. When the function that takes some time to execute is running, the user can still click the button and that function will be executed several times while the first call is still being processed.
I would like to prevent that by disabling all the buttons while that function is running but I can't get the threading to work properly.
Here is the code that I have:
def generate_text():
choice = dropdown_choice.get()
if context_obj.context_text.get() == '':
if choice == 'OpenAI':
context = 'Some random context text'
else:
context = ' '
else:
context = context_obj.context_text.get()
if choice == 'OpenAI':
progress.start(50)
progress_bar_text = Label(text='Please wait while the text is being generated',
background='#E5F2FF',
font=("Helvetica", 12))
progress_bar_text.place(relx=.2,
rely=.66,
anchor="c")
# multithreading for the OpenAI text generation
q = queue.Queue()
thread1 = Thread(target=openAI_generator.sample, args=[text_amount.get(), temperature.get(), context, q])
thread1.start()
def display_AI_text(q):
openAI_text = q.get()
generated_text.configure(state='normal')
generated_text.delete(1.0,END)
generated_text.insert(tk.END, openAI_text)
generated_text.configure(state='disabled')
progress.stop()
progress_bar_text.place_forget()
thread2 = Thread(target=display_AI_text, args=[q])
thread2.start()
In this code, thread1 is executing the function and the thread2 is taking the input from that function and displaying it.
What I would like to do is, while thread2 is being executed, all the buttons to be disabled and when the thread finishes, the buttons to become enabled again.
I have tried adding:
thread2 = Thread(target=display_AI_text, args=[q])
generate_button.config(state="disabled")
thread2.start()
and then:
thread2.join()
generate_button.config(state="normal")
but this code freezes the app. I assume that the main thread is waiting for thread2 to finish and that is why it is not responding.
Does anyone know a way to overcome this issue?
In many GUIs you can't change GUI in thread - you have to do it in main process.
You can use queue to send information to main process which will update GUI.
In Tkinter you can use
root.after(time_in_milliseconds, function_name)
to run periodically function which can check message from this queue.
Or it can periodically check
thread2.is_alive()
instead of using thread2.join() because is_alive() doesn't block code.
import tkinter as tk
from threading import Thread
import time
def long_running_function():
print('start sleep')
time.sleep(3)
print('end sleep')
def start_thread():
global t
global counter
b['state'] = 'disable'
counter = 0
t = Thread(target=long_running_function)
t.start()
check_thread()
# or check after 100ms
# root.after(100, check_thread)
def check_thread():
global counter
if not t.is_alive():
b['state'] = 'normal'
l['text'] = ''
else:
l['text'] = str(counter)
counter += 0.1
# check again after 100ms
root.after(100, check_thread)
#-----------------------------------------------------
# counter displayed when thread is running
counter = 0
root = tk.Tk()
l = tk.Label(root)
l.pack()
b = tk.Button(root, text="Start", command=start_thread)
b.pack()
root.mainloop()
Related
I am trying to implement asyncio with pysimplegui in python.
In this GUI example, two buttons (button2 and button3) emulate a long task to accomplish.
Goal:
Be able to go back to the GUI interface even if a called function (through button) takes time to return the result.
Expected result:
If button2 or button3 or both are pushed, they both continue to execute their tasks and users can go back to the GUI to carry on with other tasks.
Current result:
As soon as either button2 or button3 is pushed, the tasks is blocked and contionue until the end and the GUI hangs until the end.
import PySimpleGUI as sg
import asyncio
import time
sg.theme('Light Blue 3')
# This design pattern simulates button callbacks
# This implementation uses a simple "Dispatch Dictionary" to store events and functions
# The callback functions
async def button1():
print('Button 1 callback')
return 'nothing'
async def button2():
print('Button 2 callback')
for i in range(1,20):
await asyncio.sleep(3)
print(f"Button 2: {i}")
return f"button2 end"
async def button3():
print('Button 3 callback')
for i in range(1,10):
await asyncio.sleep(3)
print(f"Button 3: {i}")
return f"button3: end"
# Lookup dictionary that maps button to function to call
dispatch_dictionary = {'1':button1, '2':button2, '3':button3}
# Layout the design of the GUI
layout = [[sg.Text('Please click a button', auto_size_text=True)],
[sg.Button('1'), sg.Button('2'), sg.Button('3'), sg.Quit()]]
# Show the Window to the user__TIMEOUT__
window = sg.Window('Button callback example', layout)
# Event loop. Read buttons, make callbacks
while True:
# Read the Window
event, values = window.read()
if event in ('Quit', sg.WIN_CLOSED):
break
if event == '__TIMEOUT__':
continue
# Lookup event in function dictionary
if event in dispatch_dictionary:
func_to_call = dispatch_dictionary[event] # get function from dispatch dictionary
print(asyncio.run(func_to_call()))
else:
print('Event {} not in dispatch dictionary'.format(event))
window.close()
# All done!
sg.popup_ok('Done')
I thought that I applied async/wait according to the rules. Did I miss something?
asyncio.run() executes one coroutine and blocks until it is done. It does NOT start a parallel thread to run the coroutine in.
You have two options:
Don't use asyncio, use Threading to start a new thread for each long operation.
Start a thread with an asyncio eventloop in it at the start of the program, then use asyncio.run_coroutine_threadsafe() to schedule a coroutine onto the event loop from the main GUI thread.
I will explain option 2 here.. Example at the start of your program:
from threading import Thread
def asyncloop(loop):
# Set loop as the active event loop for this thread
asyncio.set_event_loop(loop)
# We will get our tasks from the main thread so just run an empty loop
loop.run_forever()
# create a new loop
loop = asyncio.new_event_loop()
# Create the new thread, giving loop as argument
t = Thread(target=asyncloop, args=(loop,))
# Start the thread
t.start()
Later in the button event code (in main thread):
asyncio.run_coroutine_threadsafe(func_to_call(), loop)
This will schedule the coroutine to run as a parallel task inside the thread we created.
I am learning python on my own and my level is probably a poor excuse for a "script kiddie" as I kinda understand and mostly end up borrowing and mashing together different scripts till it does what I want. However this is the first time I'm trying to create a GUI for one of the scripts I have. I'm using PySimpleGUI and I've been able to understand it surprisingly well. All but one thing is working the way I want it to.
The issue is I want to stop a running daemon thread without exiting the GUI. If I get the stop button to work the GUI closes and if the GUI does not close it doesn't stop the thread. The issue is between lines '64-68'. I have tried a few things and just put a place holder on line '65' to remember that I was trying to keep the GUI ("Main Thread" in my head-speak) running. The script will run in this state but the 'Stop' button does not work.
Note: I put a lot of comments in my scripts so I remember what each part is, what it does and what I need to clean up. I don't know if this is a good practice if I plan on sharing a script. Also, if it matters, I use Visual Studio Code.
#!/usr/local/bin/python3
import PySimpleGUI as sg
import pyautogui
import queue
import threading
import time
import sys
from datetime import datetime
from idlelib import window
pyautogui.FAILSAFE = False
numMin = None
# ------------------ Thread ---------------------
def move_cursor(gui_queue):
if ((len(sys.argv)<2) or sys.argv[1].isalpha() or int(sys.argv[1])<1):
numMin = 3
else:
numMin = int(sys.argv[1])
while(True):
x=0
while(x<numMin):
time.sleep(5) # Set short for debugging (will set to '60' later)
x+=1
for i in range(0,50):
pyautogui.moveTo(0,i*4)
pyautogui.moveTo(1,1)
for i in range(0,3):
pyautogui.press("shift")
print("Movement made at {}".format(datetime.now().time()))
# --------------------- GUI ---------------------
def the_gui():
sg.theme('LightGrey1') # Add a touch of color
gui_queue = queue.Queue() # Used to communicate between GUI and thread
layout = [ [sg.Text('Execution Log')],
[sg.Output(size=(30, 6))],
[sg.Button('Start'), sg.Button('Stop'), sg.Button('Click Me'), sg.Button('Close')] ]
window = sg.Window('Stay Available', layout)
# -------------- EVENT LOOP ---------------------
# Event Loop to process "events"
while True:
event, values = window.read(timeout=100)
if event in (None,'Close'):
break
elif event.startswith('Start'): # Start button event
try:
print('Starting "Stay Available" app')
threading.Thread(target=move_cursor,
args=(gui_queue,), daemon=True).start()
except queue.Empty:
print('App did not run')
elif event.startswith('Stop'): # Stop button event
try:
print('Stopping "Stay Available" app')
threading.main_thread # To remind me I want to go back to the original state
except queue.Empty:
print('App did not stop')
elif event == 'Click Me': # To see if GUI is responding (will be removed later)
print('Your GUI is alive and well')
window.close(); del window
if __name__ == '__main__':
gui_queue = queue.Queue() # Not sure if it goes here or where it is above
the_gui()
print('Exiting Program')
From this answer: create the class stoppable_thread.
Then: store the threads on a global variable:
# [...]
# store the threads on a global variable or somewhere
all_threads = []
# Create the function that will send events to the ui loop
def start_reading(window, sudo_password = ""):
While True:
window.write_event_value('-THREAD-', 'event')
time.sleep(.5)
# Create start and stop threads function
def start_thread(window):
t1 = Stoppable_Thread(target=start_reading, args=(window,), daemon=True)
t1.start()
all_threads.append(t1)
def stop_all_threads():
for thread in all_threads:
thread.terminate()
Finally, on the main window loop, handle the events that start, stop or get information from the thread.
I'm trying to read a very slow sensor (1-wire) while still operating other functions. I've replaced the read sensors with a sleep of 3 sec. I'd like to understand if I can have below time concurrently print accurate time every second while the sensor (wait 3sec.) occurs. This threading concept is new to me.
import time
import threading
from tkinter import Tk
def events_every_second(): #update clock every second
right_now = time.strftime("%I:%M:%S %p")#("%H:%M:%S")
print("time is now",right_now)
root.after(1000, events_every_second)
def one_wire():
time.sleep(3)
print("one_wire loop")
root.after(3010, one_wire)
root = Tk()
t_one_wire = one_wire()
thread_one_wire = threading.Thread(target = t_one_wire)
thread_one_wire.start()
t_ees = events_every_second
thread_ees = threading.Thread(target = t_ees)
thread_ees.start()
root.mainloop()
Just the function that does the lenghty sensor read needs to be
in a separate thread.
You can use a queue.Queue to obtain data from it -
in the example bellow I inserted the value fetching in the
every_one_sec function.
The every_one_sec function is handled by Tkinter scheduling events -
no need to creat a thread for it.
Other than that, the most incorrect part in your code was doing a
full call to one_wire before creating the thread in
t_one_wire = one_wire() - and the fact that it would run also require
tkinter to call it back. Tkinter wants to run all its events in the same
thread - so this would lead to problems.
I hope the comments bellow are enough for a better comprehension
import time
import threading
from queue import Queue, Empty
from tkinter import Tk
def some_ui_code():
global stop_reading
stop_reading = True
def events_every_second(): #update clock every second
right_now = time.strftime("%I:%M:%S %p")#("%H:%M:%S")
print("time is now",right_now)
try:
result = queue.get(block=False)
except Empty:
result = None
if result is not None:
# code to display the result read from the sensor in tkinter interface goes here
...
# tkinter, not a separate thread, handles this
root.after(1000, events_every_second)
def one_wire(queue):
# this is handled in a separate thread, and "does not know" about tkinter at all
while not stop_reading:
result = call_to_read_sensor() # takes 3 seconds
queue.put(result)
print("one_wire loop")
time.sleep(0.1) # actual call to time.sleep to space sensor readings, if needed.
root = Tk()
stop_reading = False
queue = Queue()
# start events_every_second - tkinter will handle the subsequent calls with the `root.after` scheduling
events_every_second()
thread_one_wire = threading.Thread(target=t_one_wire, args=(queue,))
thread_one_wire.start()
root.mainloop()
You should not use root.after(3010,one_wire) this will cause your Tk GUI to hang for 3 seconds and don't update.
You want to create a new Thread for this function so it does not stop your Tk app.
def one_wire():
time.sleep(3)
print("one_wire_loop")
create a new thread for this function.
import threading
one_wire_thread = threading.Thread(target = one_wire, args = ())
one_wire_thread.start()
Note that the code above will only run your function once. you could create a wrapper function with a while True: in there to keep it running, in the wrapper function you could also check for condition to stop and break the function and thus stop the one_wire function. you could also do this in the one_wire function:
def one_wire():
while True:
time.sleep(3)
print("one_wire_loop")
if {"condition to stop reading sensor"}:
break
from pythoncom import PumpWaitingMessages
import pyHook, threading
import tkinter as tk
threadsRun = 1
token = 0
def pas():
while threadsRun:
pass
def listen(startButton):
"""Listens for keystrokes"""
def OnKeyboardEvent(event):
"""A key was pressed"""
global threadsRun
if event.Key == "R":
startButton.config(relief=tk.RAISED, state=tk.NORMAL, text="Start")
threadsRun = 0
return True
hm = pyHook.HookManager()
hm.KeyDown = OnKeyboardEvent
hm.HookKeyboard()
while threadsRun:
PumpWaitingMessages()
else:
hm.UnhookKeyboard()
def simplegui():
def threadmaker():
"""Starts threads running listen() and pas()"""
startButton.config(relief=tk.SUNKEN, state=tk.DISABLED, text="r=stop listening")
global token, threadsRun
threadsRun = 1
token += 1
t1 = threading.Thread(target=pas, name='pas{}'.format(token))
t2 = threading.Thread(target=listen, args=(startButton,), name='listen{}'.format(token))
t1.start()
t2.start()
def destroy():
"""exit program"""
global threadsRun
threadsRun = 0
root.destroy()
startButton = tk.Button(root, text="Start", command=threadmaker, height=10, width=20)
startButton.grid(row=1, column=0)
quitButton = tk.Button(root, text="Quit", command=destroy, height=10, width=20)
quitButton.grid(row=1, column=1)
root = tk.Tk()
simplegui()
root.mainloop()
Code description:
simplegui() creates two threads to run
pas() and
listen()
simultaneously.
listen() waits for keyboard presses(only r does anything: exits both threads/functions).
pas() does nothing but is needed to reproduce bug.
Problem description:
After clicking start, pressing any button on the keyboard can cause tkinter to stop responding.
~2/3rd of the time r will behave as intended.
I'm using Spyder IDE (python 3.5).
Some observations:
Using print statements, the program will go into while threadsRun loop, in listen(), before the crash, but didn't reach OnKeyboardEvent() print statement.
Can wait a long time before pressing a key and it may freeze.
Can press a key instantly after pressing start and it may function as intended.
Removing t1 = ... and t1.start() lines allows program to run bug free.
Alternatively, removing all tkinter code allows program to run bug free.
Mashing a bunch of keys all at once freezes it.
If I place a print statement inside the while threadsRun loop, r will rarely work.
I've read in other posts, tkinter is not thread safe, and to use a queue. But I don't understand how. I also think maybe something else is wrong because it works sometimes. https://www.reddit.com/r/learnpython/comments/1v5v3r/tkinter_uis_toplevel_freezes_on_windows_machine/
Thanks very much for reading.
One attempt I managed to use for threads and queues is the following (replaced used code with multiple pseudo-code entries)
The Class works as a session watchdog and uses sql commands gather logged in users, then uses threads for their location detection (geoip)
class SessionWatchdog
import Tkinter as tk
import ttk
import Queue
import Locator
class SessionWatchdog(ttk.Frame):
"""
Class to monitor active Sessions including Location in a threaded environment
"""
__queue = None
__sql = None
def __init__(self, *args, **kwargs):
#...
# Create the Queue
self.__queue = Queue.Queue()
def inqueue(self):
""" Handle Input from watchdog worker Thread """
if self.__queue.empty():
return
while self.__queue.qsize():
"""
Use
try:
self.__queue.get()
finally:
self.__queue.task_done()
to retrieve data from the queue
"""
pass
def gather_data(self, queue):
"""
Retrieve online users and locate them
"""
if self.__sql:
threads = []
# gather data via sql
# ....
# ....
for data in sql_result:
thread = Locator(queue, data)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
Locator to fill the queue:
class Locator
import threading
import Queue
class Locator(threading.Thread):
"""
docstring
"""
__base_url = "http://freegeoip.net/json/{}"
def __init__(self, queue, user_information):
threading.Thread.__init__(self)
self.queue = queue
self.user_information = user_information
def run(self):
""" add location information to data (self.user_information)
to improve performance, we put the localization in single threads.
"""
located_user = []
# locate the user in a function, NOT method!
self.queue.put(located_user, False)
Here's the relevant part of the code:
x=None
def pp():
global x
x=MyClass()
x.start()
def main():
global x
p=Process(target=pp)
p.start()
while x==None:
print("Not yet...")
while 1:
print(x.getoutput(),end="")
p.join()
if __name__=='__main__':
main()
The x.start() method opens a TKInter window, so it runs forever (or at least until the user closes the window). I'm trying to run another process that would get information from the used window, but it doesn't work.
How can i make it work?
I feel like the first thing to point out here is that each child process will import the main script and have it's own local copy. It's not possible to use global variables in the way you're trying to here, because the child processes don't use the same namespace. If you're set on using multiprocessing for this, you need to use a communication pipe of some description, as described in the following documentation:
http://pymotw.com/2/multiprocessing/communication.html#multiprocessing-queues
I am a little curious what your ultimate objective is here for using multiprocessing. Still, if you really want to do it, it's possible:
import multiprocessing
import tkinter
import time
def worker(q):
#q is a queue for communcation.
#Set up tkinter:
root = tkinter.Tk()
localvar = tkinter.StringVar()
wind = tkinter.Entry(root,textvariable=localvar)
wind.grid()
#This callback puts the contents of the entry into the queue
#when the entry widget is modified
def clbk(name,index,mode,q=q):
q.put(localvar.get())
localvar.trace_variable('w',clbk)
#Some window dressing that will signal to the main process that
#the window has been closed
def close(root=root,q=q):
q.put('EXIT')
root.destroy()
root.quit()
root.protocol("WM_DELETE_WINDOW",close)
root.mainloop()
def main():
#Make a queue to facilitate communication:
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=worker,args=(queue,))
p.start()
#Wait for input...
while True:
ret = queue.get()
print(ret)
if ret=='EXIT':
break
time.sleep(0.1)
#Finally, join the process back in.
p.join()
if __name__ == '__main__':
main()
Ignoring the window dressing, that will now print text entered into your tkinter entry widget, and exits when the window is closed.