Python 使用PyQt和matplotlib快速重绘

Python 使用PyQt和matplotlib快速重绘,python,matplotlib,pyqt,blit,Python,Matplotlib,Pyqt,Blit,我正在尝试创建带有实时数据可视化的GUI应用程序 我提出的解决方案是使用著名的局部重画技巧: fig, axes = plt.subplots(nrows=2, ncols=1) axes[0].set_{labels, limits} axes[1].set_{labels, limits} fig.show() fig.canvas.draw() N = 2 graph_0 = axes[0].scatter([] * N, [] * N, s=[np.pi*5**2] * N,

我正在尝试创建带有实时数据可视化的GUI应用程序

我提出的解决方案是使用著名的局部重画技巧:

fig, axes = plt.subplots(nrows=2, ncols=1)

axes[0].set_{labels, limits}
axes[1].set_{labels, limits}    

fig.show()
fig.canvas.draw()
N = 2
graph_0 = axes[0].scatter([] * N, [] * N, s=[np.pi*5**2] * N, animated=True)
graph_1 = axes[1].scatter([] * N, [] * N, s=[np.pi*5**2] * N, animated=True)
bg_0 = fig.canvas.copy_from_bbox(axes[0].bbox)
bg_1 = fig.canvas.copy_from_bbox(axes[1].bbox)

while True:

    # Acquire new data
    tmp = [1, 2]  # *For example*

    # Clear plots
    fig.canvas.restore_region(bg_0)
    fig.canvas.restore_region(bg_1)

    # Set data to be visualized
    graph_0.set_offsets([(i+1, d) for i, d in enumerate(tmp)])
    graph_1.set_offsets([(i+1, d) for i, d in enumerate(tmp)])

    # Redraw
    axes[0].draw_artist(graph_0)
    axes[1].draw_artist(graph_1)
    fig.canvas.blit(axes[0].bbox)
    fig.canvas.blit(axes[1].bbox)
这个解决方案很好,一切正常

之后,我尝试实现相同的方法,但使用PyQt后端(添加一些控制元素)

代码如下:

import sys
from PyQt4 import QtGui
from PyQt4.uic import loadUiType

import numpy as np
import matplotlib
import matplotlib.style
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt4agg import (
    FigureCanvasQTAgg as FigureCanvas)

Ui_MainWindow, QMainWindow = loadUiType('dialog.ui')


class Main(QMainWindow, Ui_MainWindow):
    def __init__(self, ):
        super(Main, self).__init__()
        self.setupUi(self)

        self.layout_mpl = QtGui.QVBoxLayout()
        self.widget_mpl.setLayout(self.layout_mpl)

        # Assign callbacks
        self.button.clicked.connect(self.update_figure)
        self.prepare_figure()

    def prepare_figure(self):
        self.fig, self.axes = plt.subplots(nrows=2, ncols=1)
        self.canvas = FigureCanvas(self.fig)
        self.layout_mpl.addWidget(self.canvas)
        # self.canvas.draw()

        self.axes[0].set_{labels, limits}
        self.axes[1].set_{labels, limits}    

        self.graph = [None] * 2
        N = 5
        self.graph[0] = self.axes[0].scatter(
            [] * N, [] * N, s=[np.pi*5**2] * N, animated=True)
        self.graph[1] = self.axes[1].scatter(
            [] * N, [] * N, s=[np.pi*5**2] * N, animated=True)

        self.canvas.draw()
        self.bg = self.canvas.copy_from_bbox(self.fig.bbox)

        # self.bg = [None] * 2
        # self.bg[0] = self.canvas.copy_from_bbox(self.fig.axes[0].bbox)
        # self.bg[1] = self.canvas.copy_from_bbox(self.fig.axes[1].bbox)


    def update_figure(self):
        # self.canvas.restore_region(self.bg[0])
        # self.canvas.restore_region(self.bg[1])
        self.canvas.restore_region(self.bg)   

        # Set data to be visualized
        self.graph[0].set_offsets([(i+1, d) for i, d in
                                   enumerate(np.random.rand(5)*100+100)])
        self.graph[1].set_offsets([(i+1, d) for i, d in
                                   enumerate(np.random.rand(5)*100+100)])

        # Redraw
        self.axes[0].draw_artist(self.graph[0])
        self.axes[1].draw_artist(self.graph[1])

        self.canvas.blit(self.axes[0].bbox)
        self.canvas.blit(self.axes[1].bbox)


if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    main = Main()
    main.show()
    sys.exit(app.exec_())
您可能会看到我在注释行下使用的不同调用组合

因此,我希望每次点击
按钮
都会更新绘图。它实际上是有效的,除了一个阻塞问题:

a) 申请刚刚开始

b) 按钮已单击一次

c) 将焦点切换到另一个窗口并返回(此图是带有偏移的图,但点放置在旧坐标中)

有人能给我指路吗

提前谢谢


使用的版本:Python 3.4.3、matplotlib 1.4.3、PyQt 4.10.4。

解决方案是不断检查和更新轴。bbox大小:


问题在于,在完全重新调整背景大小之前,您会在某个时间点抓取背景,以便最终显示在屏幕上。你需要做的是

  • 使用
    draw_idle
    让Qt决定何时实际触发完全重画
  • 使用mpl事件堆栈在绘制之后获取背景
只是相关的方法:

    def prepare_figure(self):
        self.fig = Figure()
        self.axes = self.fig.add_subplot(111)

        self.axes.set_xlim((0, 6))
        self.axes.set_xlabel('seq.index')
        self.axes.set_ylim((0, 100))
        self.axes.set_ylabel('observation')

        N = 5
        self.graph = self.axes.scatter(
            [] * N, [] * N, s=[np.pi*5**2] * N,
            color='seagreen', animated=True)

        self.canvas = FigureCanvas(self.fig)
        # w = self.widget_mpl.width()
        # h = self.widget_mpl.height()
        # self.canvas.setFixedSize(w, h)

        self.layout_mpl.addWidget(self.canvas)

        # hook up to mpls event handling framework for draw events
        # this is emitted after the canvas has finished a full redraw
        self.canvas.mpl_connect('draw_event', self._draw_event)

        # ask the canvas to kindly draw it self some time in the future
        # when Qt thinks it is convenient
        self.canvas.draw_idle()

    def _draw_event(self, evt):
        # after drawing, grab the new background
        self.bg = self.canvas.copy_from_bbox(self.axes.bbox)

查看
动画
模块中的blit代码,了解执行此操作所需的全套功能(还有一个重新调整大小的事件),在强制重新绘制后,您需要重新blit上一次的数据。

您所做的一切看起来都是正确的。我不能100%确定您的问题来自何处,但可能导致此问题的一个问题是,当/如果调整画布大小时,您没有更新您的背景(
self.bg
)。这将导致还原错误的区域,如果图形的大小已经调整。@JoeKington我的主窗口(
对话框
)大小固定,但必须检查
小部件
。。。问题是,当我多次单击按钮时,点仍在旧坐标中生成,而栅格、轴和标签则在带有偏移的坐标中重新绘制。这就是为什么我认为这很可能是一个
matplotlib
问题。我很困惑,这实际上在1.5中没有解决。这种方法不是比
old_animation
示例中的方法更复杂吗?在您的案例中,什么作为重新绘制的触发器?这确保您仅在正确地重新绘制新背景后才能获取新背景。在这种情况下,完全绘制由Qt中的重新绘制逻辑触发(例如,图形大小调整)。