如何在一个进程中多次启动 pyqt GUI?

2022-01-12 00:00:00 python pyqtgraph pyqt pyqt5

问题描述

如何构建代码以在一个进程中连续多次运行 pyqt GUI?

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

(特别是 pyqtgraph,如果相关的话)

(pyqtgraph specifically, if that is relevant)

在测量设备上执行长时间运行的数据捕获的 python 脚本(一个大的 for 循环).在每次捕获迭代期间,都会出现一个新的 GUI,并向用户显示来自测量设备的实时数据,同时主捕获代码正在运行.

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.

我想做这样的事情:

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

主要问题

我希望数据捕获代码成为主线程.然而 pyqt 似乎不允许这种架构,因为它的 app.exec_() 是一个阻塞调用,允许每个进程只创建一次 GUI(例如,在 gui.display() 以上).

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).


解决方案

应用程序是一个可执行进程,它运行在一个或多个前台线程上,每个前台线程也可以启动后台线程来执行并行操作或操作而不阻塞调用线程.应用程序将在所有前台线程结束后终止,因此,您至少需要一个前台线程,在您的情况下,该线程是在您调用 app.exec_() 语句时创建的.在 GUI 应用程序中,这是您应该创建和显示主窗口和任何其他 UI 小部件的 UI 线程.当所有小部件都关闭时,Qt 将自动终止您的应用程序进程.

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:

启动应用程序 > 创建主窗口 > 为每个计算启动一个后台线程 > 将进度发送到 UI 线程 > 每次计算完成后在窗口中显示结果 > 关闭所有窗口 > 结束应用程序

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

另外,您应该使用 ThreadPool 来确保不会耗尽资源.

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

这是一个完整的例子:

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_())

相关文章