matplotlib и PyQt: динамическая фигура работает медленно после нескольких загрузок или выглядит беспорядочно

РЕДАКТИРОВАТЬ:

Я решил переписать это, чтобы включить рабочий пример моей проблемы. Хотя это довольно длинно, я надеюсь, что это окажется полезным для многих в будущем.

import sys

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setGeometry(100, 100, 640, 480)
        showButton = QPushButton('Show')

        toolbarShowButton = self.addToolBar('Show button toolbar')

        toolbarShowButton.addWidget(showButton)

        self.connect(showButton, SIGNAL('clicked()'), self.showButtonClicked)
        self.graphLabel = GraphCanvas(self);
        self.setCentralWidget(self.graphLabel)

    def showButtonClicked(self):  
        self.graphLabel.drawGraph()

    def resizeEvent(self, event):
        try:
            self.graphLabel.setFig()
        except AttributeError:
            pass

class GraphCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):

        self.fig = Figure(figsize=(width, height), dpi=dpi)

        self.axes = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

        self.background = None

    def drawGraph(self):
        self.axes.cla()
        self.someplot = self.axes.plot(range(1,5), range(1,5))
        self.redVert, = self.axes.plot(None, None, 'r--')
        self.greenVert, = self.axes.plot(None, None, 'g--')
        self.yellowVert, = self.axes.plot(None, None, 'y--')

        self.verticalLines = (self.redVert, self.greenVert, self.yellowVert)

        self.fig.canvas.mpl_connect('motion_notify_event', self.onMove)

        self.draw()
        self.background = self.fig.canvas.copy_from_bbox(self.axes.bbox)

    def onMove(self, event):

        # cursor moves on the canvas
        if event.inaxes:

            # restore the clean background
            self.fig.canvas.restore_region(self.background)
            ymin, ymax = self.axes.get_ylim()
            x = event.xdata - 1

            # draw each vertical line
            for line in self.verticalLines:
                line.set_xdata((x,))
                line.set_ydata((ymin, ymax))
                self.axes.draw_artist(line)
                x += 1

            self.fig.canvas.blit(self.axes.bbox)

    def setFig(self):
        '''
        Draws the canvas again after the main window
        has been resized.
        '''

        try:
            # hide all vertical lines
            for line in self.verticalLines:
                line.set_visible(False)

        except AttributeError:
            pass

        else:
            # draw canvas again and capture the background
            self.draw()
            self.background = self.fig.canvas.copy_from_bbox(self.axes.bbox)

            # set all vertical lines visible again
            for line in self.verticalLines:
                line.set_visible(True)

def main():
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

if __name__ == '__main__': main()

Описание кода

У меня есть базовый QMainWindow с панелью инструментов, на которой есть кнопка «показать». Главное окно также создает холст для фигуры matplotlib и устанавливает ее в качестве центрального виджета.

Когда пользователь нажимает кнопку «показать», некоторые данные отображаются путем вызова метода drawGraph() из GraphCanvas. В реальной программе эти данные изменяются в зависимости от того, что пользователь выбрал для отображения до нажатия кнопки. Метод resizeEvent() в основном рисует фигуру снова, чтобы приспособиться к новому размеру центрального виджета.

Метод drawGraph() создает четыре графика, первый из которых содержит данные, поэтому он виден. Последние две строки рисуют фигуру и сохраняют статический фон в переменную self.background.

Когда пользователь перемещает мышь по холсту, сначала загружается фон. Я хотел сохранить и загрузить статический фон, чтобы фигура рисовалась быстрее. После этого последние три графика получают свои данные динамически и отображаются в виде трех вертикальных линий, которые перемещаются вместе с курсором мыши.

Проблема

1) По мере того, как вы продолжаете нажимать кнопку "показать", рисунок становится все медленнее. Если вы попробуете ударить его примерно 50 раз и подвигать мышью по фигуре, вы увидите, что вертикальные линии отстают гораздо больше. Когда на реальном рисунке гораздо больше динамичных графиков, аннотаций и т. д., программа становится непригодной для использования уже через несколько кликов.

Кто-то поумнее, наверное, может сказать, почему происходит такое замедление, но я предполагаю, что ранее загруженные фигуры хранятся где-то в памяти и, возможно, рисуются под вновь созданной фигурой. И стек становится все больше и больше.

2) Рисунок отображается сразу после запуска программы. Ничего страшного, но я бы предпочел просто пустую область, пока не будет нажата кнопка.

Испытано решение

Я попытался переместить эти две строки с def __init__(self) из class MainWindow на def showButtonClicked(self):

self.graphLabel = GraphCanvas(self);
self.setCentralWidget(self.graphLabel)

Итак, теперь это выглядит так:

def showButtonClicked(self):  
    self.graphLabel = GraphCanvas(self);
    self.setCentralWidget(self.graphLabel)
    self.graphLabel.drawGraph()

Поэтому я создал фигуру только после нажатия кнопки. Это решает проблему замедления, но создает другую проблему. Теперь, когда вы нажимаете кнопку «показать» и перемещаете мышь по холсту, загружается сохраненный фон фигуры, но в исходном размере 5 на 4 дюйма, и у нас получается беспорядок. То есть в основном фон сохранялся не в том размере, в котором была нарисована фигура, а в том размере, в котором она была создана.

Однако, если я изменяю размер окна, все работает хорошо. Но в следующий раз, когда я нажимаю кнопку «показать», проблема появляется снова, и мне нужно снова изменить размер окна.

Что мне нужно

Мне нужно, чтобы эта штука работала плавно и выглядела так, как должна, независимо от того, сколько раз была нажата кнопка «показать». Кроме того, я бы предпочел, чтобы фигура не отображалась до тех пор, пока кнопка «показать» не будет нажата в первый раз, и с этого момента будет видна до закрытия программы.

Мне приходят в голову несколько приемов, например, изменение размера окна на один пиксель при нажатии кнопки «Показать», но это неправильный подход, не так ли?

Любые идеи и предложения более чем приветствуются. Спасибо.


person NotNone    schedule 31.07.2013    source источник
comment
При встраивании не импортируйте pyplot. См. matplotlib.org/examples/user_interfaces/embedding_in_qt4.html. поскольку pyplot устанавливает собственный графический интерфейс, основной цикл и холст.   -  person tacaswell    schedule 01.08.2013
comment
Спасибо за точку! Однако это не решает проблему, с которой я столкнулся, и, похоже, не имеет значения. Я переписываю вопрос с рабочим примером.   -  person NotNone    schedule 01.08.2013


Ответы (1)


Я нашел достойное решение проблемы, которое будет работать до тех пор, пока не будет найдено лучшее.

Причина, по которой решение, которое я попробовал, вызвало беспорядок, заключается в том, что после создания экземпляра GraphCanvas и установки его как QCentralWidget размер QCentralWidget сжимается до размера экземпляра GraphCanvas, который в данном случае составляет 500 * 400, и Bbox такого размера. сохраняется. Однако сама фигура использует все доступное пространство.

Когда вы создаете GraphCanvas и устанавливаете его как QCentralWidget, виджет использует размер экземпляра GraphCanvas до тех пор, пока не завершится выполнение метода, в котором он был создан (и всех его родителей). После этого они оба выстраиваются.

Если мы создадим холст в методе __init__, это не вызовет беспорядка, потому что в методе drawGraph размеры QCentralWidget и GraphCanvas совпадают, и используется правильный размер bbox. Однако, когда мы создадим его в showButtonClick, bbox будет иметь «неправильный» размер, пока метод не завершится.

Кроме того, если мы создадим QCentralWidget в методе __init__ QMainWindow, его размер будет соответствовать размеру всего окна, заданного self.setGeometry(). После того, как метод будет завершен, размер QCentralWidget будет рассчитан так, чтобы он поместился в окне, и обычно становится меньше.

Чтобы решить эту проблему, я решил создать фиктивный QWidget в методе __init__ и добавить его как QCentralWidget. Затем в методе showButtonClicked я беру ширину и высоту QCentralWidget и создаю GraphCanvas, используя сохраненную ширину и высоту. Таким образом, размеры совпадают с самого начала.

Итак, вот соответствующий код:

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setGeometry(100, 100, 640, 480)
        showButton = QPushButton('Show')

        toolbarShowButton = self.addToolBar('Show button toolbar')

        toolbarShowButton.addWidget(showButton)
        self.connect(showButton, SIGNAL('clicked()'), self.showButtonClicked)

        # dummy QWidget
        tempWidget = QWidget()
        self.setCentralWidget(tempWidget)

    def showButtonClicked(self):

        width = self.centralWidget().width()
        height = self.centralWidget().height()

        # convert to float (python 2) to prevent
        # flooring in the following divisions
        dpi = float(100)

        # create the canvas and replace the central widget
        self.graphLabel = GraphCanvas(self, width=width/dpi, height=height/dpi, dpi=dpi);
        self.setCentralWidget(self.graphLabel)

        self.graphLabel.drawGraph()
person NotNone    schedule 02.08.2013