返回
如何在 PyQt5 和 Vispy 应用中解决数据源线程更新 GUI 问题?
python
2024-03-03 03:29:16
## 解决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