如何在一个进程中连续启动多个PyQt GUI界面?

5

如何设计代码在同一进程中连续运行多次pyqt图形用户界面?(特别是pyqtgraph,如果相关)

背景

一个Python脚本在测量设备上执行长时间数据采集(一个大型循环)。在每个采集迭代过程中,新的GUI出现并向用户显示来自测量设备的实时数据,同时主要采集代码正在运行。

我想做这样的事情:

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()),所以似乎不允许这种架构。


2
我会研究线程库。使用线程池创建多个线程,每个线程显示一个图形。当用户关闭一个图形时,将该线程返回到线程池中,以便下一个线程可以使用。或者,您的主脚本可以使用“os”/“subprocess”模块启动更多进程。 - Carel
那么,您想要在每次迭代中显示GUI并在该GUI中实时显示测量数据吗? - JoshuaCS
3个回答

5
一个应用程序是可执行的进程,运行在一个或多个前台线程上,每个线程也可以启动后台线程来执行并行操作或不阻塞调用线程的操作。当所有前台线程结束后,应用程序将终止,因此您需要至少一个前台线程,在调用 app.exec_() 语句时创建。在 GUI 应用程序中,这是 UI 线程,在其中您应该创建和显示主窗口和任何其他 UI 小部件。Qt 将在所有小部件关闭后自动终止您的应用程序进程。
我的建议是尽可能遵循上述正常流程,工作流程如下:
启动应用程序 > 创建主窗口 > 为每个计算启动一个后台线程 > 将进度发送到 UI 线程 > 在每次计算完成后在窗口中显示结果 > 关闭所有窗口 > 结束应用程序
此外,您应该使用 ThreadPool 来确保您不会耗尽资源。
以下是一个完整的示例:
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_())

4
如果您想让GUI保持实时更新而不被冻结,有两种主要方法可以实现:
  1. 定时刷新GUI,在耗时函数内调用QApplication.processEvents()
  2. 创建一个单独的线程(我指的是QThread),在其中运行您的耗时函数。
个人偏好于采用后一种方式。这里是一个很好的Qt多线程入门教程。
看一下您的代码:
...
gui.display()
measurement_equipment.capture_data(300) #may take hours
gui.close()
...

看起来你正在gui.display内部调用app.exec_。很可能你需要解耦这两个函数并在调用capture_data后,在gui.display之外调用app.exec_。您还需要将新线程的finished信号连接到gui.close。代码应该类似于:

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

我希望这可以帮助你,不要迟到!!


@Ivan,请问我的回答对你有用吗?我在等待你的反馈。谢谢。 - JoshuaCS

0

你只能有一个图形用户界面线程。这意味着需要一些线程来捕获数据,并在需要时将数据与图形应用程序同步。

我们需要知道GUI数据显示是实时显示还是仅一次性显示。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接