返回

如何在 PyQt5 和 Vispy 应用中解决数据源线程更新 GUI 问题?

python

## 解决PyQt5和Vispy应用中数据源线程更新GUI问题

在使用PyQt5和Vispy创建的数据可视化应用中,有时可能会遇到GUI更新不及时的问题,尤其是在数据源线程中添加了睡眠时。本文将探讨导致此问题的原因并提供解决方法。

### 问题原因

PyQt5的事件机制

PyQt5使用事件循环来管理GUI事件。当用户与界面交互或应用程序收到新数据时,事件会被放入事件队列。主线程不断从该队列中提取事件并进行处理。

线程和事件传递

问题产生于将数据源放在一个单独的线程中。默认情况下,线程之间不会相互发送事件。这意味着数据源线程中发生的事件无法被主线程中的GUI处理。

### 解决方法

要解决此问题,我们需要让数据源线程能够将事件发送给主线程。这可以通过以下步骤实现:

使用moveToThread()移动数据源

将数据源对象移动到另一个线程,以便它能够接收和处理来自该线程的事件。

self.data_source.moveToThread(self.data_thread)

使用Qt.QueuedConnection发送信号

在数据源线程中,使用Qt.QueuedConnection发送信号,将信号排队到主线程的事件循环中。

self.data_source.new_data.connect(self.canvas_wrapper.update_data, Qt.QueuedConnection)

### 代码示例

以下代码片段展示了如何在PyQt5和Vispy应用中实现此解决方案:

import sys
from PyQt5.QtGui import QCloseEvent
from vispy.app import use_app, Timer
import numpy as np
from PyQt5 import QtWidgets, QtCore
from vispy import scene, visuals
import time
from scipy import signal
from collections import deque

class MainWindow(QtWidgets.QMainWindow):
    closing = QtCore.pyqtSignal()
    def __init__(self, canvas_wrapper, *args, **kwargs):

        super(MainWindow, self).__init__(*args, **kwargs)

        central_widget = QtWidgets.QWidget()
        main_layout = QtWidgets.QHBoxLayout()

        self.canvas_wrapper = canvas_wrapper

        main_layout.addWidget(self.canvas_wrapper.canvas.native)

        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

    def closeEvent(self, event):
        print("Closing main window")
        self.closing.emit()
        return super().closeEvent(event)


class CanvasWrapper:
    def __init__(self, update_interval = .016):

        self.canvas = scene.SceneCanvas(keys='interactive', size=(600, 600), show=True)
        self.grid = self.canvas.central_widget.add_grid()

        title = scene.Label("Test Plot", color='white')
        title.height_max = 40

        self.grid.add_widget(title, row=0, col=0, col_span=2)

        self.yaxis = scene.AxisWidget(orientation='left',
                         axis_label='Y Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=10)

        self.yaxis.width_max = 80
        self.grid.add_widget(self.yaxis, row=1, col=0)

        self.xaxis = scene.AxisWidget(orientation='bottom',
                         axis_label='X Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=20)

        self.xaxis.height_max = 80
        self.grid.add_widget(self.xaxis, row=2, col=1)

        right_padding = self.grid.add_widget(row=1, col=2, row_span=1)
        right_padding.width_max = 50

        self.view = self.grid.add_view(row=1, col=1, border_color='white')
        self.view.camera = "panzoom"

        self.data = np.empty((2, 2))
        self.line = scene.Line(self.data, parent=self.view.scene)


        self.xaxis.link_view(self.view)
        self.yaxis.link_view(self.view)

        self.update_interval = update_interval
        self.last_update_time = time.time()


    def update_data(self, newData):
        if self.should_update():

            data_array = newData["data"]
            data_array = np.array(data_array)

            x_min, x_max = data_array[:, 0].min(), data_array[:, 0].max()
            y_min, y_max = data_array[:, 1].min(), data_array[:, 1].max()


            self.view.camera.set_range(x=(x_min, x_max), y=(y_min, y_max))
            self.line.set_data(data_array)



    def should_update(self):
        current_time = time.time()
        if current_time - self.last_update_time >= self.update_interval:
            self.last_update_time = current_time
            return True
        return False


class DataSource(QtCore.QObject):
    new_data = QtCore.pyqtSignal(dict)
    finished = QtCore.pyqtSignal()

    def __init__(self, sample_rate, seconds, seconds_to_display=15, q = 100, parent = None):
        super().__init__(parent)
        self.count = 0
        self.q = q
        self.should_end = False
        self.sample_rate = sample_rate

        self.num_samples = seconds*sample_rate
        self.seconds_to_display = seconds_to_display

        size = self.seconds_to_display*self.sample_rate

        self.buffer = deque(maxlen=size)



    def run_data_creation(self):
        print("Run Data Creation is starting")
        for count in range (self.num_samples):
            if self.should_end:
                print("Data saw it was told to end")
                break

            self.update(self.count)
            self.count += 1

            data_dict = {
                "data": self.buffer,
            }
            self.new_data.emit(data_dict)

        print("Data source finished")
        self.finished.emit()

    def stop_data(self):
        print("Data source is quitting...")
        self.should_end = True

    def update(self, count):

        x_value = count / self.sample_rate
        y_value = np.sin((count / self.sample_rate) * np.pi)

        self.buffer.append([x_value, y_value])



class Main:
    def __init__(self, sample_rate, seconds, seconds_to_display):
        # Set up the application to use PyQt5
        self.app = use_app("pyqt5")
        self.app.create()

        # Set some parameters for the data source
        self.sample_rate, self.seconds, self.seconds_to_display = sample_rate, seconds, seconds_to_display

        # Create the canvas wrapper and main window
        self.canvas_wrapper = CanvasWrapper()
        self.win = MainWindow(self.canvas_wrapper)

        # Set up threading for the data source
        self.data_thread = QtCore.QThread(parent=self.win)
        self.data_source = DataSource(self.sample_rate, self.seconds)

        # Move the data source to the thread
        self.data_source.moveToThread(self.data_thread)

        # Connect signals and slots for data updates and thread management
        self.setup_connections()

    def setup_connections(self):
        self.data_source.new_data.connect(self.canvas_wrapper.update_data, Qt.QueuedConnection)
        self.data_thread.started.connect(self.data_source.run_data_creation)
        self.data_source.finished.connect(self.data_thread.quit, QtCore.Qt.DirectConnection)
        self.win.closing.connect(self.data_source.stop_data, QtCore.Qt.DirectConnection)
        self.data_thread.finished.connect(self.data_source.deleteLater)

    def run(self):
        self.win.show()
        self.data_thread.start()

        self.app.run()
        self.data_thread