Web Analytics Made Easy - Statcounter
Skip to content

Multithreading

IMPORTANT GUI Topic!

Brief summary:

PySimpleGUI can help you with running long operations as threads without you needing to learn the threading library. The Window method perform_long_operation makes this serious GUI problem a non-issue.

Threads can "inject" events and data into a window.read() call. This allows your GUI application to simply stop, pend and awaken immediately when something happens. This makes for zero CPU time used when northing's happening and it means 0ms latency. In other words, you're not polling, you're pending.

The Long Operation

A classic problem of GUI programming is when you try to perform some operation that requires a lot of time. The problem is simple enough.... you have a GUI and when you press a button, you want a 10 second operation to take place while you're GUI patiently waits.

What happens to most people that give this a try gets the dreaded Windows/Linux/Mac "Your program has stopped responding do you wish to close it"

If you add a sleep(30) to your code, it's not very many seconds before your window does this:

SNAG-0866

No Bueno

PySimpleGUI Window.perform_long_operation

To get you over the initial hump of multi-threaded programming, you can let PySimpleGUI create and manage threads for you. Like other APIs in PySimpleGUI, it's been simplified down as far as possible.

Here's the basic steps using perform_long_operation 1. Pass your function name and a key to the call to window.perform_long_operation 2. Continue running your GUI event loop 3. Windows pend using their typical window.read() call 4. You will get the event when your function returns 5. The values dictionary will contain your function's return value. They key will be the same as the event. So, values[event] is your function's return value.

import PySimpleGUI as sg
import time

# My function that takes a long time to do...
def my_long_operation():
    time.sleep(15)
    return 'My return value'


def main():
    layout = [  [sg.Text('My Window')],
                [sg.Input(key='-IN-')],
                [sg.Text(key='-OUT-')],
                [sg.Button('Go'), sg.Button('Threaded'), sg.Button('Dummy')]  ]

    window = sg.Window('Window Title', layout, keep_on_top=True)

    while True:             # Event Loop
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break

        window['-OUT-'].update(f'{event, values}')  # show the event and values in the window
        window.refresh()                            # make sure it's shown immediately

        if event == 'Go':
            return_value = my_long_operation()
            window['-OUT-'].update(f'direct return value = {return_value}')
        elif event  == 'Threaded':
            # Let PySimpleGUI do the threading for you...
            window.perform_long_operation(my_long_operation, '-OPERATION DONE-')
        elif event  == '-OPERATION DONE-':
            window['-OUT-'].update(f'indirect return value = {values[event]}')

    window.close()

if __name__ == '__main__':
    main()

What if my function takes parameters?

Note that the first parameter for perform_long_operation is your function. If you're like most of us, you'll enter my_func() instead of my_func. The first actually calls your function immediately, the second passes your function's object rather than calling it.

If you need to pass parameters to your function, then you'll need to make one simple change... add a lambda. Think of it as how you would want your function called.

In the Demo Program for this call, Demo_Long_Operations.py, it uses a function that takes parameters as the example. Here is the line of code from that demo:

# This is where the magic happens.  Add your function call as a lambda
window.perform_long_operation(lambda :
                              my_long_func(int(values['-IN-']), a=10),
                              '-END KEY-')

I've broken the code up with newlines to emphasize where your function call goes. A more common format may be:

window.perform_long_operation(lambda : my_long_func(int(values['-IN-']), a=10), '-END KEY-')

Here is the function definition that is to be called:

def my_long_func(count, a=1, b=2):

The Thread-based Solution

If you're ready to jump on into threading, then you can do that too.

Here's the basic steps 1. You put your long-running operation into a thread 2. Your thread signals the window when it is done 3. Windows pend using their typical window.read() call 4. The values dictionary will contain your function's return value if you pass it through

Take a moment to get to know the code. You'll find the typical event loop. If you run this program, and you don't touch anything like your mouse, then it should sit for 10 seconds doing nothing and then print out the completed the message.

SNAG-0867

If you attempted to interact with the window by pressing the "Nothing" button, then you will likely get a message about your window stopped responding.

Threaded Long Operation

I think we can agree that brute force, no matter how badly we want it to work, won't. Bummer

import PySimpleGUI as sg
import time
import threading


def long_function_thread(window):
    time.sleep(10)
    window.write_event_value('-THREAD DONE-', '')

def long_function():
    threading.Thread(target=long_function_thread, args=(window,), daemon=True).start()


layout = [[sg.Output(size=(60,10))],
          [sg.Button('Go'), sg.Button('Nothing'), sg.Button('Exit')]  ]

window = sg.Window('Window Title', layout)

while True:             # Event Loop
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Go':
        print('About to go to call my long function')
        long_function()
        print('Long function has returned from starting')
    elif event == '-THREAD DONE-':
        print('Your long operation completed')
    else:
        print(event, values)
window.close()

If you click the "Nothing" button, then you'll get a line printed in the Multiline that has the event and the values dictionary.

Because there are no "input" elements, your values dictionary is empty.

Clicking "Go" is when the fun begins.

You are immediately shown a message that the long-operation function is starting. The same function name as before is called long_function. But now the contents of that function have been replaced with starting a thread that executes the same code.

This single line of code is all that was needed to create our long0runing function as a thread and to start that thread:

threading.Thread(target=the_thread, args=(window,), daemon=True).start()

The conversion over to a thread was done in 3 simple steps:

  1. Renamed the long_fundtion to long_function_thread
  2. Pass into the long_function_thread the window that it will communicate with
  3. Add call to window.write_event_value when the long_running_thread is existing

The result is a GUI that continues to operate and be responsive to user's requests during the long running operation.

Long operations with feedback

The power of the Window.write_event_value is that it can be used at any time, not just at the beginning and end of operations. If a long operation can be broken into smaller parts, then progress can be shown to the user. Rather than calling Window.write_event_value one time, it can be called a number of times too.

If we modify the code so that instead of sleeping for 10 seconds, we sleep for 1 second 10 times, then it's possible to show information about progress.

Here's the code with the new operation broken up into 10 parts

import PySimpleGUI as sg
import time
import threading

def long_function_thread(window):
    for i in range(10):
        time.sleep(1)
        window.write_event_value('-THREAD PROGRESS-', i)
    window.write_event_value('-THREAD DONE-', '')

def long_function():
    threading.Thread(target=long_function_thread, args=(window,), daemon=True).start()


layout = [[sg.Output(size=(60,10))],
          [sg.Button('Go'), sg.Button('Nothing'), sg.Button('Exit')]  ]

window = sg.Window('Window Title', layout)

while True:             # Event Loop
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Go':
        print('About to go to call my long function')
        long_function()
        print('Long function has returned from starting')
    elif event == '-THREAD DONE-':
        print('Your long operation completed')
    else:
        print(event, values)
window.close()

And the resulting window

image