Pyside Plotting с пользовательским виджетом FigureCanvas - PullRequest
0 голосов
/ 21 мая 2019

Прежде всего, я новичок в Pyside и приветствую конструктивные отзывы.Я также ценю любого, кто смотрит на это и пытается запустить мой код.Заранее спасибо!

Я должен также упомянуть, что я использую https://github.com/mottosso/Qt.py в качестве импорта Qt в моем коде.

Я создаю графический интерфейс для выполнения некоторых аппаратных модульных тестов.Этот графический интерфейс имеет встроенный график FigureCanvas для отображения данных, полученных с оборудования, когда пользователь нажимает кнопку.Я создаю подкласс unittest.TestCase для вставки PlotCanvas (пользовательский класс FigureCanvas с некоторыми простыми методами).Затем я создаю мастер с несколькими страницами, каждая из которых имеет независимый график.

Теперь это работает, если я не делаю подкласс unittest.TestCase и просто создаю статический plot_canvas.См. DEBUG_STATE = 1 в моем главном блоке.

# Uses statically defined plot canvas, no subclassing
class TestPage1(unittest.TestCase):

    PLOT_COLS = 1
    PLOT_SIZE = (12, 6)
    plot_canvas = PlotCanvas(size=PLOT_SIZE, cols=PLOT_COLS)

    def test_01(self):
        x = np.linspace(0,10,50)
        self.plot_canvas.plot(x, 2*x)

Но если я использую StaticPlottingTestCase, код практически идентичен, я не могу вставить несколько страниц.Холст не отображается на первой странице.Я думаю, что это как-то связано с QWizard, но я не уверен.См. DEBUG_STATE = 3.

class StaticPlottingTestCase(unittest.TestCase):
    # Static definition
    PLOT_COLS = 1
    PLOT_SIZE = (12, 6)
    plot_canvas = PlotCanvas(size=PLOT_SIZE, cols=PLOT_COLS)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

В попытке исправить это я создал другой класс InstancePlottingTestCase, для которого plot_canvas определен как переменная класса.Теперь сюжет появляется на обеих страницах, но черчение ничего не дает.См. DEBUG_STATE = 4.

class InstancePlottingTestCase(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Instance definition
        self.plot_canvas = PlotCanvas()

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

Это мой минимальный рабочий пример.Переменная DEBUG_STATE в моем главном блоке может иметь разные значения для имитации проблем, с которыми я сталкиваюсь.

DEBUG_STATE = 1: Мой базовый подход.Это работает, как и ожидалось.

DEBUG_STATE = 2: одна страница StaticPlottingTestCase.Это работает.

DEBUG_STATE = 3: две страницы StaticPlottingTestCase.На первой странице нет сюжета.Построение на первой странице, похоже, влияет на вторую.

DEBUG_STATE = 4: две страницы InstancePlottingTestCase.Графики отображаются, но они не могут быть нарисованы.

ОБНОВЛЕНИЕ: я обнаружил, что если я добавлю две страницы с одинаковыми именами (DEBUG_STATE = 6), результат будет таким же, как DEBUG_STATE = 3. Интересно...

Полный код:

import io
import sys
import unittest
from unittest.runner import TextTestResult

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

from Qt import QtCore, QtGui, QtWidgets
from Qt.QtCore import QCoreApplication
from Qt.QtWidgets import (QFormLayout, QPushButton, QVBoxLayout, QWizard,
                          QWizardPage)


def discover_tests(test_case):
    """Compile a list of tests for the given test case.

    Parameters
    ----------
    test_case : TestCase

    Returns
    -------
    names : list
        Test names

    """
    return [a for a in dir(test_case) if a.startswith('test_')]


class PlotCanvas(FigureCanvas):

    def __init__(self, figsize=(8,6), *args, **kwargs):
        # Initialize the figure first
        self.fig = Figure(figsize=figsize)
        self.fig.set_facecolor('white')

        # Create the canvas with the created figure
        super(PlotCanvas, self).__init__(self.fig)

        # Add axes to the figure
        self.fig.add_subplot(111)
        self.ax = self.fig.get_axes()[0]

    def plot(self, x, y, *args, **kwargs):
        self.ax.clear()
        self.ax.plot(x, y)
        self.draw()


class StaticPlottingTestCase(unittest.TestCase):
    # Static definition
    PLOT_COLS = 1
    PLOT_SIZE = (12, 6)
    plot_canvas = PlotCanvas(size=PLOT_SIZE, cols=PLOT_COLS)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

class InstancePlottingTestCase(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Instance definition
        self.plot_canvas = PlotCanvas()


class TestPage(QWizardPage):

    def __init__(self, unittest_class, static_canvas=True, *args, **kwargs):
        super(TestPage, self).__init__(*args, **kwargs)

        # Assign the unit test class
        self.unittest_class = unittest_class

        # Define overall layout as an hbox
        top_hbox = QtWidgets.QHBoxLayout()

        # Populate left side with unittest buttons
        unittest_vbox = QVBoxLayout()
        formLayout = QFormLayout()
        self.buttons = {}
        unittest_names = discover_tests(unittest_class)
        for name in unittest_names:
            button = QPushButton('Push to Start', self)
            button.setCheckable(True)
            button.unittest_name = name
            button.setMinimumWidth(100)
            button.clicked.connect(self.onButton)
            self.buttons[name] = button
            formLayout.addRow(name + ':', button)
        unittest_vbox.addLayout(formLayout)

        # Central plot
        plot_vbox = QVBoxLayout()

        # Static plot
        if static_canvas:
            self.plot_canvas = self.unittest_class.plot_canvas
        else:
            self.plot_canvas = self.unittest_class().plot_canvas

        plot_vbox.addWidget(self.plot_canvas)

        # Set overall layout
        top_hbox.addLayout(unittest_vbox)
        top_hbox.addLayout(plot_vbox)
        self.setLayout(top_hbox)

    def onButton(self):
        """ Create a TestSuite with just the clicked test. 
        """
        name = self.sender().unittest_name
        self.runTests({name : self.buttons[name]})
        # print(self.plot_canvas.figure.)

    def runTests(self, buttons):
        """ Run the unit tests corresponding to the dict buttons.
        Buttons are used instead of names because the isChecked() value provides
        pass/fail information about the test, whereas a name is just...a name.
        """
        suite = unittest.TestSuite()
        stream = io.StringIO()

        for name, button in buttons.items():
            suite.addTest(self.unittest_class(name))

        runner = unittest.TextTestRunner(
            stream=stream, 
            verbosity=2, 
            failfast=True,
            resultclass=TextTestResult,
        )
        runner.run(suite)


if __name__ == '__main__':

    # Uses statically defined plot canvas, no subclassing
    class TestPage1(unittest.TestCase):

        PLOT_COLS = 1
        PLOT_SIZE = (12, 6)
        plot_canvas = PlotCanvas(size=PLOT_SIZE, cols=PLOT_COLS)

        def test_01(self):
            x = np.linspace(0,10,50)
            self.plot_canvas.plot(x, 2*x)

    class TestPage1b(unittest.TestCase):

        PLOT_COLS = 1
        PLOT_SIZE = (12, 6)
        plot_canvas = PlotCanvas(size=PLOT_SIZE, cols=PLOT_COLS)

        def test_01(self):
            x = np.linspace(0,10,50)
            self.plot_canvas.plot(x, 2*x)

    # Uses statically defined canvas in subclass. Should act the same way as TestPage1.
    class TestPage2(StaticPlottingTestCase):

        def test_01(self):
            x = np.linspace(0,10,50)
            self.plot_canvas.plot(x, 3*x)

    class TestPage2b(StaticPlottingTestCase):

        def test_01(self):
            x = np.linspace(0,10,50)
            self.plot_canvas.plot(x, 3*x)

    # Uses instance defined canvas. This doesn't plot anything.
    class TestPage3(InstancePlottingTestCase):

        def test_01(self):
            x = np.linspace(0,10,50)
            y = x ^ 2
            self.plot_canvas.plot(x, y)

    class TestPage3b(InstancePlottingTestCase):

        def test_01(self):
            x = np.linspace(0,10,50)
            y = x ^ 2
            self.plot_canvas.plot(x, y)

    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)

    print('Conjuring test wizard...')
    wizard = QWizard()
    wizard.setWizardStyle(QtWidgets.QWizard.ModernStyle)

    # Debugging
    DEBUG_STATE = 6

    # Adding TestPage1 works as expected. The plots are independent.
    if DEBUG_STATE == 1:
        wizard.addPage(TestPage(TestPage1, static_canvas=True))
        wizard.addPage(TestPage(TestPage1b, static_canvas=True))

    # A single TestPage2 works...
    elif DEBUG_STATE == 2:
        wizard.addPage(TestPage(TestPage2, static_canvas=True))

    # ...but adding two of them does not work...
    # The canvas doesn't show up on the first page. 
    # I think this has something to do with the widgets layout.
    elif DEBUG_STATE == 3:
        wizard.addPage(TestPage(TestPage2, static_canvas=True))
        wizard.addPage(TestPage(TestPage2b, static_canvas=True))

    # Making self.canvas a class variable generates the plot, but now plotting doesn't do anything
    elif DEBUG_STATE == 4:
        wizard.addPage(TestPage(TestPage3, static_canvas=False))
        wizard.addPage(TestPage(TestPage3b, static_canvas=False))

    # Plotting works on the statically defined canvas, but not on the class-defined one.
    elif DEBUG_STATE == 5:
        wizard.addPage(TestPage(TestPage2, static_canvas=True))
        wizard.addPage(TestPage(TestPage3, static_canvas=False))

    # A clue! Adding a page with the same name mimics DEBUG_STATE == 3...
    elif DEBUG_STATE == 6:
        wizard.addPage(TestPage(TestPage1, static_canvas=True))
        wizard.addPage(TestPage(TestPage1, static_canvas=True))


    wizard.setFixedSize(1200, 800)
    wizard.show()
    app.exec_()
...