Search code examples
pythonuser-interfacepyqtpyqt5simpy

Reading user's input during simulation via GUI


I'm in the process of converting a simulation based application using simpy into a GUI.

The program currently runs within the console and the simpy which is doing the simulation runs by default in a loop like syntax. And that's where my issue seems to be.

Currently, within the console version of the code, I grab user input through the raw_input() function and that is able to interrupt the code and allow the user to input the values that the simulation desires. However, despite researching it, there does not seem to be a similar and clean way of doing this through pyqt inisde the GUI I'm building.

Would the only way be to run the processes in different threads? And if I were to do that approach, how exactly would that look and truly function?


Solution

  • PyQt is event-based. It's running a loop continuously waiting for events, and it calls your callbacks (or signals your slots) when it gets an event you care about. So, there's no way to directly say "block until I get input".

    But, before you even get to that point, if your simulation is running a loop continuously in the main thread, PyQt can't also be running a loop continuously in the main thread. So it can't respond to events from the OS like "update your window" or "quit". As far as your user is concerned, the app is just frozen; she'll see nothing but the ever-popular beachball (or other platform equivalent).

    And however you choose to solve that first problem will solve most of the second one almost for free.


    Why your GUI app freezes attempts to explain the whole issue, and all of the possible solutions, in general terms, using Tkinter as an example of a GUI library. If you want something more Qt-specific, I'm pretty sure there's a whole section about it in the Qt tutorial, although I'm not sure where, and you may have to translate a bit of C++ to Python in your head.

    But there are two major options: Callbacks, or threads.


    First, you can break your loop up into small pieces, each of which only takes a few milliseconds. Instead of running the whole loop, you run the first piece, and as its last line, it asks PyQt to schedule the next piece as soon as possible (e.g., using a QTimer with a timeout of 0). Now, Qt will get to check for events every few milliseconds, and if it's got nothing to do, it will immediately kick off the next step of your work.

    If your flow control is already built around an iterator (or push-coroutine) that yields appropriately-sized chunks, this is very easy. If not, it can mean turning the flow control in your outer loop inside-out, which can be hard to understand.

    So, having done this, how do you get user input? Simple:

    • Where you would have called raw_input, instead of scheduling the next piece of your code, instead do some appropriate GUI stuff—create a popup messagebox, unhide a text entry control and button, whatever.
    • Connect the next piece of your code as a handler for the button-clicking or messagebox-accepting or whatever signal.

    Alternatively, you can run your work in a background thread. This doesn't require you to reorganize anything, but it does require you to be careful not to share anything between threads. Unfortunately, this includes calling methods on GUI widgets from the background thread, which you'd think would make it impossible to do anything useful. Fortunately, PyQt has mechanisms to deal with that pretty easily: signals are automatically routed between threads as necessary.

    So, how do you ask for user input in this scenario?

    • Split off everything after the raw_input into a separate function, which you connect as a handler for a got_input signal.
    • In the original function, where you used to call raw_input, you instead emit a gimme_input signal.
    • Write a handler for that gimme_input signal to run in the main thread, which will put up the GUI widgets (as with the single-threaded example above).
    • Write a handler for the OK button that emits the got_input signal back to the worker thread.