matplotlib与pyqt4:在一个线程更新图表,在另一个线程绘制
我正在用PyQt4和matplotlib创建一个实时数据监控的图形界面(GUI),用来生成图表。这个界面一次可以显示多个图表(大约6到7个)。为了让GUI线程有更多的时间响应得更快,我只在主线程中执行canvas.draw()
,而其他绘图命令则在更新图表数据的线程中执行。因此,在非GUI线程中,我会执行像line.set_ydata
、ax.set_ylim
这样的命令,来更新需要改变的内容。
这两个线程通过一个在初始化时传递给它们的字典来访问图形和画布对象。当非GUI线程获取数据并更新图表后,它会通过Qt的信号机制通知GUI线程重新绘制画布(自动连接)。根据我对多线程的经验,我知道应该使用锁,或者确保非GUI线程在某种方式上被阻塞,以等待重新绘制,但因为我在编写代码时比较匆忙,所以没有加上这个,直到现在才想起来。这个情况的关键是,我希望能看到每一次图表的更新,而不是在另一个线程更新的过程中重新绘制,甚至错过某次更新(这样说应该能理解吧)。目前,我觉得我只是运气好,时间上刚好合适,事情看起来还不错。
还有一点可能有用的是,我是通过使用moveToThread把一个QObject移动到QThread来创建线程的。
我想问的问题是:
- 我只是运气好,还是Qt在做一些神奇的事情?
- 在matplotlib的画布/图形上,最好的阻塞方法是什么?
我应该提到,这次是我第一次尝试让GUI更灵敏(把matplotlib的命令移到数据线程中),我可能会考虑使用blit动画风格,只更新变化的图表部分。但我仍然很好奇我到底有多幸运。
谢谢大家的帮助。
更新/澄清/评论后的继续:我希望整个监控系统能让科学家们轻松修改和更新,他们可能只熟悉matlab和matplotlib。我并不完全反对为了速度而改用pyqwt进行绘图。至于每秒帧数,我其实不需要很多,因为绘制的数据每0.5秒才会到一次(最快是0.2秒)。GUI的响应似乎因为有太多图表而“吃掉了”性能。我已经用matplotlib的blitting做了一个概念验证,效果似乎好很多,如果需要的话我会考虑使用pyqwt。之前的问题依然存在。
1 个回答
我遇到了类似的问题。我需要绘制很多图表(大约250个),所以我的图形界面在Windows上总是卡住。为了改善这个情况,我修改了图形类,让绘图在一个单独的线程中进行。结果是,我的图形界面不再卡顿,绘图窗口在绘制完成后就会显示出来。使用时,你只需要创建一个PlotDialog
实例,然后调用draw_plots
方法,并传入plot_data
参数。plot_data
是一个字典列表,每个字典代表一个子图。每个子图都有以下几个键(和对应的数据):title
(标题)、xlabel
(横坐标标签)、ylabel
(纵坐标标签)和data
(数据)。希望这样解释能让你明白。
这是我的代码:
import math
from PyQt4 import QtCore, QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class MyFigure(Figure, QtCore.QThread):
def __init__(self, parent, *args, **kwargs):
QtCore.QThread.__init__(self, parent)
Figure.__init__(self, *args, **kwargs)
self.plot_data = list()
def start_plotting_thread(self, plot_data, on_finish=None):
""" Start plotting """
self.plot_data = plot_data
if on_finish is not None:
self.finished.connect(on_finish)
self.start()
def run(self):
""" Run as a thread """
# Figure out rows and columns
total_plots = len(self.plot_data)
columns = int(math.sqrt(total_plots))
if columns < 1:
columns = 1
rows = int(total_plots / columns)
if (total_plots % columns) > 0:
rows += 1
if rows < 1:
rows = 1
# Plot Data
for plot_index, _plot_data in enumerate(self.plot_data):
plot_number = plot_index + 1
args = (rows, columns, plot_number)
kwargs = {
'title': _plot_data['title'],
'xlabel': _plot_data['xlabel'],
'ylabel': _plot_data['ylabel']
}
figure = self.add_subplot(*args, **kwargs)
figure.plot(_plot_data['data'])
class PlotDialog(QtGui.QDialog):
def __init__(self, parent):
super(PlotDialog, self).__init__(parent, QtCore.Qt.WindowMinMaxButtonsHint | QtCore.Qt.WindowCloseButtonHint)
self.figure = MyFigure(self)
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
self.layout = QtGui.QGridLayout()
self.setLayout(self.layout)
layout = [
[self.canvas],
[self.toolbar],
]
for row_index, columns in enumerate(layout):
if type(columns) is list:
for column_index, widget in enumerate(columns):
if widget is not None:
self.layout.addWidget(widget, row_index, column_index)
def draw_plots(self, plot_data):
""" Plot Plots """
self.figure.start_plotting_thread(plot_data, on_finish=self.finish_drawing_plots)
def finish_drawing_plots(self):
""" Finish drawing plots """
self.canvas.draw()
self.show()