PySimpleGUI. RuntimeError: main thread is not in main loop - python

As a Python learner I decided to try out PySimpleGUI, and wrote a script in which the relevant snippet is:
`
import PySimpleGUI as sg
....
window = sg.Window('Output Filename Creator').Layout(layout)
while True:
event, values = window.Read()
if event is None or event == "Cancel":
window.Close()
sys.exit()
else:
outfile = values['file']
window.Close()
return outfile `
I use Windows 10, Python 3.7, Idle 3.7, and PySimpleGUI-3.24.0. After running the script that contains the snippet above (no execution errors) I go to the Idle shell and try to type in len('1'). At entering the open bracket the following error is generated:
Traceback (most recent call last):
File "C:\Users\Paul\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 332, in __del__
if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
RuntimeError: main thread is not in main loop
I know that PySimpleGUI is based on tkinter, but that is as far as my knowledge goes. I don't know how threading works in Python or how PySimpleGUI is interfaced with Tk. Yet I would like to know where the error comes from and what I can do to avoid it.
Update: the code reduced to bare essentials still gives the same error when window is closed by clicking on cross in upper right corner:
def OutputFileName(default):
import PySimpleGUI as sg
layout = [
[sg.In(default, key='file', size=(70,1)),
sg.SaveAs('Browse')],
[sg.Save(), sg.Text(' '*35), sg.Cancel()]
]
window = sg.Window(' ').Layout(layout)
event, values = window.Read()
if event is None or event == "Cancel":
return
elif event == 'Save':
return values['file']
outf = OutputFileName('foo.txt')

This problem is due to IDLE being written using tkinter.
tkinter is very picky about resources and threads. There is a warning in the PySimpleGUI documentation about utilizing PySimpleGUI in a threaded environment because you can get into situations like this one where resources are freed in the incorrect thread or tkinter gets confused about who is running the mainloop.
Here is an older post that talks about problems running multiple mainloops when using IDLE.
https://groups.google.com/forum/#!topic/comp.lang.python/kr7lKj4qMl4

Related

Issue with multithreading in Tkinter

I'm currently trying to create a program where it detects the mouse movement on my computer and pops up a window if any movement is detected. However, I've run into some issues with Tkinter threading. I have read multiple other posts on the same issue and understand that Tkinter isn't thread-safe and that it can only be run in the main thread. However, I'm still unsure about the fix for my program. I'd appreciate it if someone could help me take a look.
Essentially, my (simplified) program has two scripts, access_detection.py and password_prompt.py. The first file is responsible for using pynput.mouse to track mouse movement, and the second file is responsible for popping up a Tkinter window and prompting the user for a password whenever mouse movement from the first script is detected.
> password_prompt.py (pops up a window for the user to enter their passcode)
def confirm_passcode(self):
self.access_granted = True
def prompt_password(self) -> bool:
# initialize window
entry = Entry(self.win, width=25, textvariable=self.password, show="*")
entry.pack(pady=((int(self.win.winfo_screenheight()/2) - 100), 0))
button = ttk.Button(self.win, text="Confirm Password", command=self.confirm_passcode)
button.pack(pady=10)
while True:
self.win.attributes('-fullscreen', True)
self.win.attributes('-topmost', True)
if self.access_granted == True:
return True
elif self.access_granted == False and self.access_attempts >= 3:
return False
self.win.update()
> access_detection.py (tracks mouse movement)
def on_move_mouse(self, x, y):
self.security_passed = popup_obj.prompt_password()
if self.security_passed == False:
# do something here
else:
# stops this program
def detection_start(self):
with mouse_listener(on_move=self.on_move_mouse) as mouse_monitor:
mouse_monitor.join()
# driver
if __name__ == "__main__":
monitor = # a class with detection_start()
monitor.detection_start()
The issue raised: RuntimeError: main thread is not in main loop
Currently, the main method is in access_detection.py and I believe the pynput.mouse threading is using the main thread, causing the error since Tkinter isn't thread-safe. Is there a way that I could have these two threads running simultaneously and still accomplish the goal of tracking mouse movement and popping up a window when movement is detected? (with Tkinter in the main thread if it has to be necessary).
The original code is a bit lengthy. It could be found here if you need more info. (ignore main.py)

pygame.error: video system not initialized, when threading is used [duplicate]

This question already has an answer here:
Multithreading with Pygame
(1 answer)
Closed 1 year ago.
Recently i completed learning about threading in python and now trying to implement it in a program. And here is the code:
from threading import Thread
from time import sleep
import pygame as py
py.init()
X,Y = 900, 800
APP = py.display.set_mode((X, Y))
APP = py.display.set_caption("Test")
def exit_app():
"To terminate program."
while True:
for eve in py.event.get():
if eve.type == py.QUIT:
py.quit()
exit()
def update_display():
"Update pygame window"
while True:
sleep(3)
py.display.update()
if __name__ == "__main__":
Thread(target=exit_app).start()
Thread(target=update_display).start()
Now the problem is that after creating thread i ran into a problem.
When i tried to run code it get executed without any problem but when the code exit(i.e., when i close the pygame window) i get to see the error as follows:
$python3 test.py
pygame 2.0.1 (SDL 2.0.14, Python 3.9.4)
Hello from the pygame community. https://www.pygame.org/contribute.html
Exception in thread Thread-2:
Traceback (most recent call last):
File "/usr/lib/python3.9/threading.py", line 954, in _bootstrap_inner
self.run()
File "/usr/lib/python3.9/threading.py", line 892, in run
self._target(*self._args, **self._kwargs)
File "/home/cvam/Documents/test.py", line 23, in update_display
py.display.update()
pygame.error: video system not initialized
Ok, so first of all it seems like you are trying to create separate functions with infinite loops for updating the screen and for event handling, to clear the same, all of it can be done in one loop and is perhaps better way of doing the same.[Thus I have removed the exit_app and the update_display function since they use two other infinite while loops.]
Secondly there is no need to use a separate thread for event handling, since the event handling can be done in the same loop, when you create a separate thread for it with an infinite while loop, then that results in confusion as to which thread pygame shall run on, the main thread or the THREAD1.[Thus the same has been commented out.]
Also placing pygame.init function call and all other statements that were before outside inside the if name == 'main' block seems to ensure that they are executed. Also placing them before the thread starts makes sure that they are executed and that pygame stuff is initialized before the thread executes.
Also some suggestions -:
Instead of the sleep function from the time module, a similar function of the pygame time module namely the pygame.time.delay function can be used, its present within pygame and thus is more suitable to use.
I have experienced many errors while quitting the pygame programs using the inbuilt exit and quit functions, and thus seem to prefer using exit function of the sys module of python since it leads to less errors in most cases.
The code for the same shall become -:
# from threading import Thread
# from time import sleep
import pygame
import sys
if __name__ == "__main__":
pygame.init()
X,Y = 900, 800
APP = pygame.display.set_mode((X, Y))
APP = pygame.display.set_caption("Test")
# # By detecting the events within the main game loop there is no need
# # to call a separate thread, and the same may not be working since the two
# # threads the main one and the THREAD1 are both containing infinite loops and pygame
# # can only take over one at a time.
# THREAD1=Thread(target=exit_app)
# THREAD1.start()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit() # sys module's exit method seems to be better and causes less errors at exit
# # Without a delay the window will still not close incase this script is being
# # used for that purpose by you.
# pygame.time.delay(3) # Pygame alternative to sleep method of time module https://www.pygame.org/docs/ref/time.html#pygame.time.delay
pygame.display.update()
continue
pass

Setting variables which is not entered in GUI and "Not Responding" Problem

I wanted to write a program that moves the mouse cursor and presses the shift key so that the computer won't be locked. The Problem is I want that if the user doesn't set two variables they should be automatically 0 and 1 but it seems I am missing something. The other thing that I can't understand is when I start the program if I don't click on it everything is ok, but if I click on the program's window it says "Not Responding" but the program is running. I can't understand what I am doing wrong here. Below is my code:
#!/usr/bin/python
import pyautogui
import time
import sys
import os
import PySimpleGUI as sg
from datetime import datetime
def main():
sg.theme('DarkAmber')
layout = [ [sg.Text('Please enter the time intervall between the movements:', size = (45,1)), sg.Input(key='-IT-', enable_events=True)],
[sg.Text('Please enter how long should the script run:', size = (45,1)), sg.Input(key='-DURATION-', enable_events=True)],
[sg.Button('Start'), sg.Button('Stop')],
[sg.Output(size=(60,15))] ]
window = sg.Window('Press to start to move!', layout, size=(450,250), element_justification='right')
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Stop'):
break
if event == '-IT-' and values['-IT-'] and values['-IT-'][-1] not in ('0123456789'):
window['-IT-'].update(values['-IT-'][:-1])
if event == '-DURATION-' and values['-DURATION-'] and values['-DURATION-'][-1] not in ('0123456789'):
window['-DURATION-'].update(values['-DURATION-'][:-1])
elif event == 'Start':
if values['-IT-'] == "" and values['-DURATION-'] == "":
window['-IT-'].update(1)
window['-DURATION-'].update(0)
elif values['-IT-'] != "" and values['-DURATION-'] == "":
window['-DURATION-'].update(0)
elif values['-IT-'] == "" and values['-DURATION-'] != "":
window['-IT-'].update(1)
move(numMin=int('0'+values['-IT-']), numDuration=int('0'+values['-DURATION-']))
window.close()
main()
Edit:
I've just cut out the unnecessary part of the code.
So if I'm not misunderstanding, I need to use a thread, the second question is for me unclear can I typecast an empty string without adding zero (0) to it? Because when I try just typecast it doesn't work, and if I add a zero the behavior of the code is changing.
Operations That Take a "Long Time"
If you're a Windows user you've seen windows show in their title bar "Not Responding" which is soon followed by a Windows popup stating that "Your program has stopped responding". Well, you too can make that message and popup appear if you so wish! All you need to do is execute an operation that takes "too long" (i.e. a few seconds) inside your event loop.
You have a couple of options for dealing this with. If your operation can be broken up into smaller parts, then you can call Window.Refresh() occasionally to avoid this message. If you're running a loop for example, drop that call in with your other work. This will keep the GUI happy and Window's won't complain.
If, on the other hand, your operation is not under your control or you are unable to add Refresh calls, then the next option available to you is to move your long operations into a thread.
The "Old Way"
There are a couple of demo programs available for you to see how to do this. You basically put your work into a thread. When the thread is completed, it tells the GUI by sending a message through a queue. The event loop will run with a timer set to a value that represents how "responsive" you want your GUI to be to the work completing.
The "New Way" - Window.write_event_value
This new function that is available currently only in the tkinter port as of July 2020 is exciting and represents the future way multi-threading will be handled in PySimpleGUI (or so is hoped).
Previously, a queue was used where your event loop would poll for incoming messages from a thread.
Now, threads can directly inject events into a Window so that it will show up in the window.read() calls. This allows a your event loop to "pend", waiting for normal window events as well as events being generated by threads.
You can see this new capability in action in this demo: Demo_Multithreaded_Write_Event_Value.py
Here is that program for your inspection and education. It's SO nice to no longer poll for threaded events.
import threading
import time
import PySimpleGUI as sg
"""
Threaded Demo - Uses Window.write_event_value communications
Requires PySimpleGUI.py version 4.25.0 and later
This is a really important demo to understand if you're going to be using multithreading in PySimpleGUI.
Older mechanisms for multi-threading in PySimpleGUI relied on polling of a queue. The management of a communications
queue is now performed internally to PySimpleGUI.
The importance of using the new window.write_event_value call cannot be emphasized enough. It will hav a HUGE impact, in
a positive way, on your code to move to this mechanism as your code will simply "pend" waiting for an event rather than polling.
Copyright 2020 PySimpleGUI.org
"""
THREAD_EVENT = '-THREAD-'
cp = sg.cprint
def the_thread(window):
"""
The thread that communicates with the application through the window's events.
Once a second wakes and sends a new event and associated value to the window
"""
i = 0
while True:
time.sleep(1)
window.write_event_value('-THREAD-', (threading.current_thread().name, i)) # Data sent is a tuple of thread name and counter
cp('This is cheating from the thread', c='white on green')
i += 1
def main():
"""
The demo will display in the multiline info about the event and values dictionary as it is being
returned from window.read()
Every time "Start" is clicked a new thread is started
Try clicking "Dummy" to see that the window is active while the thread stuff is happening in the background
"""
layout = [ [sg.Text('Output Area - cprint\'s route to here', font='Any 15')],
[sg.Multiline(size=(65,20), key='-ML-', autoscroll=True, reroute_stdout=True, write_only=True, reroute_cprint=True)],
[sg.T('Input so you can see data in your dictionary')],
[sg.Input(key='-IN-', size=(30,1))],
[sg.B('Start A Thread'), sg.B('Dummy'), sg.Button('Exit')] ]
window = sg.Window('Window Title', layout, finalize=True)
while True: # Event Loop
event, values = window.read()
cp(event, values)
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event.startswith('Start'):
threading.Thread(target=the_thread, args=(window,), daemon=True).start()
if event == THREAD_EVENT:
cp(f'Data from the thread ', colors='white on purple', end='')
cp(f'{values[THREAD_EVENT]}', colors='white on red')
window.close()
if __name__ == '__main__':
main()
Multithreaded Programs
While on the topic of multiple threads, another demo was prepared that shows how you can run multiple threads in your program that all communicate with the event loop in order to display something in the GUI window. Recall that for PySimpleGUI (at least the tkinter port) you cannot make PySimpleGUI calls in threads other than the main program thread.
The key to these threaded programs is communication from the threads to your event loop. The mechanism chosen for these demonstrations uses the Python built-in queue module. The event loop polls these queues to see if something has been sent over from one of the threads to be displayed.
You'll find the demo that shows multiple threads communicating with a single GUI is called:
Demo_Multithreaded_Queued.py
Once again a warning is in order for plain PySimpleGUI (tkinter based) - your GUI must never run as anything but the main program thread and no threads can directly call PySimpleGUI calls.

Getting a Jupyter notebook to listen to input events while running a continuous process?

I'm trying to do something in a Jupyter notebook that runs a continuous process, but with a pause button to interrupt it. Below is a simplified version of what I've done so far, but it seems Ipython wants to complete the entire run() function before it executes commands it receives from the button. The problem, of course, being that run() will never finish unless interrupted.
Interestingly, the below strategy works just fine in my Tkinter frontend so long as I put a pause(0.0001) at the end of the updateGraph() function. Architecturally, I'd be curious why Tkinter is willing to listen to input events during that pause but Jupyter isn't. But more importantly, is there a way to get Jupyter to listen while running run()?
from ipywidgets import Button
from IPython.display import display
startstop = Button(description='Run')
startstop.click = run
display(startstop)
def run(b=None):
running=True
while running:
#Do stuff and update display
updateGraph()
startstop.click = pause
startstop.description = 'Pause'
def pause(b=None):
running = False
startstop.click = run
startstop.description = 'Run'
I prefer using Keyboard for this purpose. It is a much simpler approach to the same problem...
import keyboard
def run():
running=True
while running:
pass # some code here...
if keyboard.is_pressed('alt'):
break
run()
Press Alt key anytime to stop the execution of the program.

Unable to exit tkinter app when using "wait_variable()"

I have a python code that includes tkinter window and other running tasks.
I've been trying to bind "WM_DELETE_WINDOW" event to a function that exits my python code when I close the window but can't achieve that.
This is what I try:
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
The window is destroyed successfully but the python code doesn't exit. Any possible reason for sys.exit() not to work?
What am I doing wrong? any alternative approach should I try?
Doing some testing I figured out what can be the problem.
Here's a small code that summarizes my code which is much bigger.
import tkinter as tk
import sys
root = tk.Tk()
submitted = tk.IntVar()
def on_exit():
root.destroy()
sys.exit()
root.protocol('WM_DELETE_WINDOW', on_exit)
def submit():
submitted.set(1)
print("submitted")
button= tk.Button(root, text="Submit",command=submit)
button.pack()
button.wait_variable(submitted)
root.mainloop()
I believe now that wait_variable is the source of the problem.
And the code actually exits when I added submitted.set(1) to on_exit() ( or if I clicked the button first before closing the window ) but if I tried closing the window without pressing the button, the code won't exit.
So does this mean that wait_variable not only makes tkinter app wait, but also prevents python code exiting?!
I tried os._exit(1) and it worked, but I think it's not clean.
As your updated question points out the problem is wait_variable(). Going off the documentation for this method wait_variable() enters a local event loop that wont interrupt the mainloop however it appears that until that local event loop is terminated (the variable is updated in some way) it will prevent the python instance from terminating as there is still an active loop. So in order to prevent this you have also correctly pointed out you need to update this variable right before you terminate the tk instance.
This might seam odd but it is the behavior I would expect. It is my understanding that an active loop needs to be terminated before a python instance can exit.
As Bryan has pointed out in the comments the wait_variable() method is "a function which calls the vwait command inside the embedded tcl interpreter. This tcl interpreter knows nothing about python exceptions which is likely why it doesn't recognize the python exception raised by sys.exit()"
Link to relevant documentation:
wait_variable()
Relevant text from link:
wait_variable(name)
Waits for the given Tkinter variable to
change. This method enters a local event loop, so other parts of the
application will still be responsive. The local event loop is
terminated when the variable is updated (setting it to it’s current
value also counts).
You can also set the variable to whatever it is currently set as to terminate this event loop.
This line should work for you:
submitted.set(submitted.get())
That said you do not actually need sys.exit(). You can simply use root.destroy().
You new function should look like this:
def on_exit():
submitted.set(submitted.get())
root.destroy()
The python instance will automatically close if there is no more code after the mainloop.

Categories

Resources