0

I've been tooling around with PySimpleGUI for just a week now. I have not been able to decipher what DemoProgram(s) will help achieve my goal. Skills are novice/tinkerer.

Project: RasberryPi OpenCV workflow using:

  1. HDMI bridge, initiated with shell script when external image source is configured
  2. Capture shell script, grabbing frames from HDMI bridge into data folder
  3. Watcher.py script, watches data folder and processes new images via OpenCV script
  4. Dashboard, displayed using Chromium

Goal: GUI with button(s) to launch each step (1-4) of the process and give Real-time Output for the watcher.py script + ability to exit capture and watcher scripts which are long running

Attempted Solutions from https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms

  1. Demo_Desktop_Floating_Toolbar.py
  2. Demo_Script_Launcher_Realtime_Output.py
  3. Demo_Multithreaded_Animated_Shell_Command.py
  4. Demo_Multithreaded_Multiple_Threads.py
  5. Demo_Multithreaded_Different_Threads.py

My attempts so far have yielded mixed results. I have been able to get individual steps to launch but I have not been able to put a combination of calls, triggers, and an event loop together that works.

My hunch is that using Demo_Multithreaded_Multiple_Threads.py is where I need to be building but trying to combine that with Demo_Script_Launcher_Realtime_Output.py for just the watcher.py script has led to hanging.

#!/usr/bin/python3
import subprocess
import threading
import time
import PySimpleGUI as sg

"""
    You want to look for 3 points in this code, marked with comment "LOCATION X". 
    1. Where you put your call that takes a long time
    2. Where the trigger to make the call takes place in the event loop
    3. Where the completion of the call is indicated in the event loop
    Demo on how to add a long-running item to your PySimpleGUI Event Loop
    If you want to do something that takes a long time, and you do it in the 
    main event loop, you'll quickly begin to see messages from windows that your
    program has hung, asking if you want to kill it.
    
    The problem is not that your problem is hung, the problem is that you are 
    not calling Read or Refresh often enough.
    
    One way through this, shown here, is to put your long work into a thread that
    is spun off, allowed to work, and then gets back to the GUI when it's done working
    on that task.
    
    Every time you start up one of these long-running functions, you'll give it an "ID".
    When the function completes, it will send to the GUI Event Loop a message with 
    the format:
        work_id ::: done
    This makes it easy to parse out your original work ID
    
    You can hard code these IDs to make your code more readable.  For example, maybe
    you have a function named "update_user_list()".  You can call the work ID "user list".
    Then check for the message coming back later from the work task to see if it starts
    with "user list".  If so, then that long-running task is over. 
    
"""

# ############################# User callable CPU intensive code #############################
# Put your long running code inside this "wrapper"
# NEVER make calls to PySimpleGUI from this thread (or any thread)!
# Create one of these functions for EVERY long-running call you want to make


def long_function_wrapper(work_id, window):
    # LOCATION 1
    # this is our "long running function call"
    # sleep for a while as a simulation of a long-running computation

    def process_thread():
        global proc
        proc = subprocess.Popen('python watcher.py data', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    def main():
        thread = threading.Thread(target=process_thread, daemon=True)
        thread.start()
    
        while True:
            sg.popup_animated(sg.DEFAULT_BASE64_LOADING_GIF, 'Watcher is Running', time_between_frames=100)
            thread.join(timeout=.1)
            if not thread.is_alive():
                break
        sg.popup_animated(None)
    
        output = proc.__str__().replace('\\r\\n', '\n')
        sg.popup_scrolled(output, font='Courier 10')
    
    
    if __name__ == '__main__':
        main()

    # at the end of the work, before exiting, send a message back to the GUI indicating end
    window.write_event_value('-THREAD DONE-', work_id)
    # at this point, the thread exits
    return


############################# Begin GUI code #############################
def the_gui():
    sg.theme('Light Brown 3')


    layout = [[sg.Text('Multithreaded Work Example')],
              [sg.Text('Click Go to start a long-running function call')],
              [sg.Text(size=(25, 1), key='-OUTPUT-')],
              [sg.Text(size=(25, 1), key='-OUTPUT2-')],
              [sg.Text('?', text_color='blue', key=i, pad=(0,0), font='Default 14') for i in range(4)],
              [sg.Button('Go'), sg.Button('Popup'), sg.Button('Exit')], ]

    window = sg.Window('Multithreaded Window', layout)
    # --------------------- EVENT LOOP ---------------------
    work_id = 0
    while True:
        # wait for up to 100 ms for a GUI event
        event, values = window.read()
        if event in (sg.WIN_CLOSED, 'Exit'):
            break
        if event == 'Go':           # clicking "Go" starts a long running work item by starting thread
            window['-OUTPUT-'].update('Starting long work %s' % work_id)
            window[work_id].update(text_color='red')
            # LOCATION 2
            # STARTING long run by starting a thread
            thread_id = threading.Thread(
                target=long_function_wrapper,
                args=(work_id, window,),
                daemon=True)
            thread_id.start()
            work_id = work_id+1 if work_id < 19 else 0

        # if message received from queue, then some work was completed
        if event == '-THREAD DONE-':
            # LOCATION 3
            # this is the place you would execute code at ENDING of long running task
            # You can check the completed_work_id variable
            # to see exactly which long-running function completed
            completed_work_id = values[event]
            window['-OUTPUT2-'].update(
                'Complete Work ID "{}"'.format(completed_work_id))
            window[completed_work_id].update(text_color='green')

        if event == 'Popup':
            sg.popup_non_blocking('This is a popup showing that the GUI is running', grab_anywhere=True)
    # if user exits the window, then close the window and exit the GUI func
    window.close()

############################# Main #############################


if __name__ == '__main__':
    the_gui()
    print('Exiting Program')

(cv) pi@raspberrypi:~/issgrab $ python GUIDE_GUI_master.py
*** Faking timeout ***
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.5/threading.py", line 862, in run
    self._target(*self._args, **self._kwargs)
  File "GUIDE_GUI_master.py", line 68, in long_function_wrapper
    main()
  File "GUIDE_GUI_master.py", line 57, in main
    sg.popup_animated(sg.DEFAULT_BASE64_LOADING_GIF, 'Watcher is Running', time_between_frames=100)
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 15797, in PopupAnimated
    transparent_color=transparent_color, finalize=True, element_justification='c', icon=icon)
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 6985, in __init__
    self.Finalize()
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 7510, in Finalize
    self.Read(timeout=1)
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 7325, in Read
    results = self._read(timeout=timeout, timeout_key=timeout_key)
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 7365, in _read
    self._Show()
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 7199, in _Show
    StartupTK(self)
  File "/home/pi/.virtualenvs/cv/lib/python3.5/site-packages/PySimpleGUI/PySimpleGUI.py", line 12100, in StartupTK
    my_flex_form.TKroot.mainloop()
  File "/usr/lib/python3.5/tkinter/__init__.py", line 1143, in mainloop
    self.tk.mainloop(n)
RuntimeError: Calling Tcl from different appartment

Exiting Program
  • 2
    most GUIs don't like to run in `threads` and all widgets should be changed/updates only in main thread. If you want to change/update widget in thread then you should use `queue` to send information to main thread and in main thread you should run some function (ie. using `root.after(milliseconds, function_name)` ) which periodically check `queue` and gets information to change/update widget. – furas Jul 28 '20 at 06:37
  • 2
    There's an entirely new multi-threaded capability recently added to PySimpleGUI. It uses a call write_event_value that can be called from a thread. This removes the need to "poll" for events. The multi-threaded demo programs are all being re-written to use the new architecture. It's discussed in a new section of the Cookbook - https://pysimplegui.readthedocs.io/en/latest/cookbook/#recipe-long-operations-multi-threading – Mike from PSG Jul 28 '20 at 08:59

0 Answers0