Non-blocking PyQT as an additional GUI that visualizes the result from a main process?

Viewed 187

I have a main process that does some stuff (e.g. analyzing data) and it runs alone just fine. I also want to make a simple GUI that displays the result of the main task using PyQT5. The idea is that the GUI should not interfere in the main process, that is, if I remove the GUI it shouldn't cause any issue to the main process.

The code of the main process is quite simple:

if __name__ == '__main__':
    # initialize the object that performs the main task
    tasker = Task()

    # the graphical interface to visualize the result of tasker
    gui = GUI(task)  # GUI is a separate class that keeps a reference to tasker

    # read the input data and do stuff on each new data instance
    for f in listdir(inrepo):
        data = read_new_data(f)  # an utility function that reads new data from file
        result = tasker.process(data)  # tasker processes the new data and return some results
        gui.update(data, result)  # pass the data and result in the GUI to update it

The code of the GUI class is quite long so I only paste a few lines here, but the lines I skip are just to create widgets, nothing fancy (I didn't connect any event yet)

class GUI(QApplication):

    def __init__(self, tasker):
        """Initialize the application"""

        super().__init__([])

        self.tasker = tasker

        # define the main window
        self.window = QWidget()
        self.window.setWindowTitle('GUI')

        ...  # layout and components etc.

        # show stuff
        self.window.show()
        self.exec()

So I want the GUI to be completely independent from my main process. For example, if I don't need the GUI anymore, I can just comment out the 2 lines gui = GUI(task) and gui.update(data, result).

However, the problem is that starting the GUI blocks the entire process (I assume it's because of self.exec() in GUI.__init__, so my main process cannot proceed to loop over the data. Could you please show me how to make PyQT non-blocking? Is it even feasible?

Some options I have considered:

  1. Threading: it seems more complicated than necessary for my use case and it may make referencing to the task instance difficult from a thread. All new updates to task should be reflected in the GUI. If I'm not mistaken, PyQT's application already runs on a thread. So multi-level threading may be troublesome.

  2. Run the GUI from another Python process, communicated via shared folders: may incur high latency. Any new data and result from task should be immediately reflected in the GUI. Writing to file then reading from file then updating the GUI will cause some delay.

  3. Perform the task in GUI: I could use some timeout event to read new data periodically and run task on them, but then everything depends heavily on the GUI, and I can't just comment it out if I don't need the GUI anymore.

Any suggestion is very much appreciated! Thank you very much!

1 Answers

Switching your program between GUI mode/console mode is often not as simple as commenting out some lines. PyQt in particular does not allow you to run a GUI loop from anything other than a main thread. Not all hope is lost, though - this simply means that you should decide as early as possible whether your program is going to run as a console application or as a GUI.

Instead of relying in commenting out code, you can create a "switch" of sorts inside your code that tells your code how to execute. One way to do this is to check the command-line arguments when you execute the code, e.g.:

import sys

if "--headless" in sys.argv[1:]:  # checking the command-line arguments
    run_code_without_gui()
else:
    run_code_with_gui()

This way, executing your code as python mycode.py --headless will execute it without the GUI (through the run_code_without_gui function), while executing python mycode.py will run it as a GUI (through the run_code_with_gui function). (Although if you're actually going to parse command-line arguments, I recommend using the argparse library).

You can even keep the analysis code completely decoupled from the GUI code, so that you simply run a function from, say, analysis.py when executing without the GUI, and have the GUI call that exact same function when e.g. the user clicks on the "Analyze" button.

Another thing to note is that if your analysis code takes long to execute, it may inadvertently block the GUI. In this case, you should run the analysis code in a separate "worker" thread that spawns upon clicking the "Analyze" button, keeping the GUI responsive while it executes. This might be the way to go if you want the analysis to keep running alongside the GUI indefinitely - create the worker thread for the analysis at the same time that you display the GUI to the user.

Related