Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I launch pyqt GUI multiple times consequtively in a process?

How can I architect code to run a pyqt GUI multiple times consecutively in a process?

(pyqtgraph specifically, if that is relevant)

The context

A python script that performs long running data capture on measurement equipment (a big for loop). During each capture iteration a new GUI appear and displays live data from the measurement equipment to the user, while the main capture code is running.

I'd like to do something like this:

for setting in settings:
  measurement_equipment.start(setting)
  gui = LiveDataStreamGUI(measurement_equipment)
  gui.display()
  measurement_equipment.capture_data(300) #may take hours
  gui.close()

The main issue

I'd like the data capture code to be the main thread. However pyqt doesn't seems to allow this architecture, as its app.exec_() is a blocking call, allowing a GUI to be created only once per process (e.g., in gui.display() above).

like image 220
Ivan Avatar asked Dec 03 '18 21:12

Ivan


People also ask

Is PyQt good for GUI?

PyQt is a Python binding for Qt, which is a set of C++ libraries and development tools that include platform-independent abstractions for Graphical User Interfaces (GUI), as well as networking, threads, regular expressions, SQL databases, SVG, OpenGL, XML, and many other powerful features.

Is PyQt a GUI widgets toolkit?

PyQt is a Python binding of the cross-platform GUI toolkit Qt, implemented as a Python plug-in. PyQt is free software developed by the British firm Riverbank Computing.

How do I create a window in PyQt5?

We set the window size using the setGeometry(left,top,width,height) method. The window title is set using setWindowTitle(title). Finally show() is called to display the window.

What is PyQt5 framework?

What is PyQt5? PyQt is a library that lets you use the Qt GUI framework from Python. Qt itself is written in C++. By using it from Python, you can build applications much more quickly while not sacrificing much of the speed of C++. PyQt5 refers to the most recent version 5 of Qt.


Video Answer


2 Answers

An application is an executable process that runs on one or more foreground threads each of which can also start background threads to perform parallel operations or operations without blocking the calling thread. An application will terminate after all foreground threads have ended, therefore, you need at least one foreground thread which in your case is created when you call the app.exec_() statement. In a GUI application, this is the UI thread where you should create and display the main window and any other UI widget. Qt will automatically terminate your application process when all widgets are closed.

IMHO, you should try to follow the normal flow described above as much as possible, the workflow could be as follows:

Start Application > Create main window > Start a background thread for each calculation > Send progress to UI thread > Show results in a window after each calculation is finished > Close all windows > End application

Also, you should use ThreadPool to make sure you don't run out of resources.

Here is a complete example:

import sys
import time
import PyQt5

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QRunnable, pyqtSignal, QObject
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDialog


class CaptureDataTaskStatus(QObject):
    progress = pyqtSignal(int, int)  # This signal is used to report progress to the UI thread.
    captureDataFinished = pyqtSignal(dict)  # Assuming your result is a dict, this can be a class, a number, etc..


class CaptureDataTask(QRunnable):
    def __init__(self, num_measurements):
        super().__init__()

        self.num_measurements = num_measurements
        self.status = CaptureDataTaskStatus()

    def run(self):
        for i in range(0, self.num_measurements):
            # Report progress
            self.status.progress.emit(i + 1, self.num_measurements)
            # Make your equipment measurement here
            time.sleep(0.1) # Wait for some time to mimic a long action

        # At the end you will have a result, for example
        result = {'a': 1, 'b': 2, 'c': 3}

        # Send it to the UI thread
        self.status.captureDataFinished.emit(result)


class ResultWindow(QWidget):
    def __init__(self, result):
        super().__init__()

        # Display your result using widgets...
        self.result = result

        # For this example I will just print the dict values to the console
        print('a: {}'.format(result['a']))
        print('b: {}'.format(result['b']))
        print('c: {}'.format(result['c']))


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.result_windows = []

        self.thread_pool = QtCore.QThreadPool().globalInstance()

        # Change the following to suit your needs (I just put 1 here so you can see each task opening a window while the others are still running)
        self.thread_pool.setMaxThreadCount(1)

        # You could also start by clicking a button, menu, etc..
        self.start_capturing_data()

    def start_capturing_data(self):
        # Here you start data capture tasks as needed (I just start 3 as an example)
        for setting in range(0, 3):
            capture_data_task = CaptureDataTask(300)
            capture_data_task.status.progress.connect(self.capture_data_progress)
            capture_data_task.status.captureDataFinished.connect(self.capture_data_finished)
            self.thread_pool.globalInstance().start(capture_data_task)


    def capture_data_progress(self, current, total):
        # Update progress bar, label etc... for this example I will just print them to the console
        print('Current: {}'.format(current))
        print('Total: {}'.format(total))

    def capture_data_finished(self, result):
        result_window = ResultWindow(result)
        self.result_windows.append(result_window)
        result_window.show()


class App(QApplication):
    """Main application wrapper, loads and shows the main window"""

    def __init__(self, sys_argv):
        super().__init__(sys_argv)

        self.main_window = MainWindow()
        self.main_window.show()


if __name__ == '__main__':
    app = App(sys.argv)   
    sys.exit(app.exec_())
like image 143
Isma Avatar answered Oct 08 '22 23:10

Isma


If you want your GUI to keep updating in realtime and to not be freezed, you have two main ways to do it:

  1. Refresh the GUI from time to time calling QApplication.processEvents() inside your time consuming function.
  2. Create a separate thread (I mean, QThread) where you run your time consuming function

My personal preference is to go for the latter way. Here is a good tutorial for getting started on how to do multi-threading in Qt.

Having a look at your code:

...
gui.display()
measurement_equipment.capture_data(300) #may take hours
gui.close()
...

it seems you are calling app.exec_ inside gui.display. Its very likely you will have to decouple both functions and call app.exec_ outside of gui.display and after calling capture_data. You will also have to connect the finished signal of the new thread to gui.close. It will be something like this:

...
gui.display() # dont call app.exec_ here
thread = QThread.create(measurement_equipment.capture_data, 300)
thread.finished.connect(gui.close)
app.exec_()
...

I hope this can help you and to not be late!!

like image 41
JoshuaCS Avatar answered Oct 08 '22 21:10

JoshuaCS