Обновление графика полярной диаграммы в GUI интерфейсе при изменении данных в таблице

В программе написанной на PyQt5 интерфейсом (создавал в Qt Designer), хотел бы использовать полярную диаграмму, с обновлением графика, если вносятся изменения в ячейки таблицы.

Сама таблица с одним столбцом и 12-ю ячейками, созданная с помощью QTableView, QAbstractTableModel, с использованием QSpinDelegat, для ввода только числовых данных.

Долго искал примеры в направлении matplotlib.animation. В основном эти примеры представлены вне GUI интерфейса. В общих чертах есть понимание, как это работает. Но не понятно как связать matplotlib.animation с FigureCanvas и обновлять функцию. Или ловить сигнал от изменяемых ячеек таблицы QTableView, как это сделать?

Вопрос, как связать эти два события: ввод таблицу значений <-> обновление диаграммы? Буду признателен за любую подсказку, пример или в каком направлении искать ответ!

v1_gui.py

from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_CalculatorForm(object):
def setupUi(self, CalculatorForm):
    CalculatorForm.setObjectName("CalculatorForm")
    CalculatorForm.setWindowModality(QtCore.Qt.NonModal)
    CalculatorForm.resize(615, 460)
    CalculatorForm.setMinimumSize(QtCore.QSize(420, 460))
    palette = QtGui.QPalette()
    brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
    brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
    brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
    brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
    brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
    brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
    brush.setStyle(QtCore.Qt.SolidPattern)
    palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
    CalculatorForm.setPalette(palette)
    font = QtGui.QFont()
    font.setFamily("Arial")
    font.setPointSize(10)
    CalculatorForm.setFont(font)
    CalculatorForm.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
    CalculatorForm.setFocusPolicy(QtCore.Qt.NoFocus)
    icon = QtGui.QIcon()
    icon.addPixmap(QtGui.QPixmap("../../ОБР_ТРЕУГ/повтор/Новая папка/1.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
    CalculatorForm.setWindowIcon(icon)
    CalculatorForm.setToolTipDuration(-3)
    CalculatorForm.setAutoFillBackground(True)
    CalculatorForm.setInputMethodHints(QtCore.Qt.ImhNone)
    CalculatorForm.setIconSize(QtCore.QSize(40, 24))
    self.centralwidget = QtWidgets.QWidget(CalculatorForm)
    self.centralwidget.setObjectName("centralwidget")
    self.layoutWidget = QtWidgets.QWidget(self.centralwidget)
    self.layoutWidget.setGeometry(QtCore.QRect(0, 0, 2, 2))
    self.layoutWidget.setObjectName("layoutWidget")
    self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
    self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
    self.horizontalLayout.setObjectName("horizontalLayout")
    self.layoutWidget1 = QtWidgets.QWidget(self.centralwidget)
    self.layoutWidget1.setGeometry(QtCore.QRect(0, 0, 2, 2))
    self.layoutWidget1.setObjectName("layoutWidget1")
    self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget1)
    self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
    self.horizontalLayout_2.setObjectName("horizontalLayout_2")
    self.tableView = QtWidgets.QTableView(self.centralwidget)
    self.tableView.setGeometry(QtCore.QRect(60, 40, 121, 331))
    self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.AnyKeyPressed|QtWidgets.QAbstractItemView.CurrentChanged|QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed)
    self.tableView.setAlternatingRowColors(True)
    self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
    self.tableView.setObjectName("tableView")
    self.MplWidget = MplWidget(self.centralwidget)
    self.MplWidget.setGeometry(QtCore.QRect(210, 20, 351, 341))
    self.MplWidget.setObjectName("MplWidget")
    CalculatorForm.setCentralWidget(self.centralwidget)

    self.retranslateUi(CalculatorForm)
    QtCore.QMetaObject.connectSlotsByName(CalculatorForm)

def retranslateUi(self, CalculatorForm):
    _translate = QtCore.QCoreApplication.translate
    CalculatorForm.setWindowTitle(_translate("CalculatorForm", "Метод треугольника-обратный расчет"))
from mplwidget import MplWidget

mplwidget.py

from PyQt5.QtWidgets import QWidget, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure

class MplWidget(QWidget):
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)
        self.canvas = FigureCanvas(Figure())
        vertical_layout = QVBoxLayout()
        vertical_layout.addWidget(self.canvas)
        self.canvas.axes = self.canvas.figure.add_subplot(111, polar = True)
        self.setLayout(vertical_layout)
        self.canvas.axes.set_theta_offset(3.14/2.0)
        self.canvas.axes.set_theta_direction(-1)

main_dd.py

import sys
from PyQt5 import QtWidgets
from v1_gui import Ui_CalculatorForm
from math import cos, sin, sqrt,  radians, degrees, acos, pi
from matplotlib.backends.backend_qt5agg import (NavigationToolbar2QT as NavigationToolbar)

from PyQt5.QtWidgets import (QApplication, QWidget, QTableView, QPushButton,
QStyledItemDelegate,QSpinBox, QMenu)
from PyQt5.QtGui import QFont,QColor
from PyQt5.QtCore import (Qt, QAbstractTableModel, QModelIndex, QAbstractItemModel)
import numpy as np


class NpModel(QAbstractTableModel):
    def __init__(self, data = np.array([[]]), parent=None):
        super().__init__()
        self.npdata = data

    def rowCount(self,index=QModelIndex()):
        return len(self.npdata)

    def columnCount(self,index=QModelIndex()):
        return len(self.npdata[0])

    def data(self,index,role):
        if not index.isValid(): return None
        row = index.row()
        col = index.column()
        val = self.npdata[row,col]
        if role == Qt.DisplayRole or role == Qt.EditRole:
            return str(round(val,3))
        elif role == Qt.BackgroundRole:
            if col == 0: return QColor(0,0,0,8)

    def flags(self,index):
        if not index.isValid(): return Qt.ItemIsEnabled
        if index.column() == 0:
            return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
        return Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self,index,value,role):
        if not index.isValid():
             return False
        if role == Qt.EditRole:
            try : f = float(value.replace(',','.'))
            except: return False
            self.npdata[index.row(),index.column()] = f
            self.dataChanged.emit(index,index)
            return True
        return False

    def headerData(self,section,orientation,role):
        if role != Qt.DisplayRole: return None
        if orientation == Qt.Horizontal:
            return 'TF' + str(section + 1)
        else: return str(section + 1)

    def load(self,arr):
        self.beginResetModel()
        self.npdata = arr
        self.endResetModel()

class SpinDelegate(QStyledItemDelegate):
    def createEditor(self, parent, options, index):
        editor = QSpinBox(parent)
        editor.setFrame(False)
        editor.setRange(0,360)
        editor.setSingleStep(1)
        return editor

    def setEditorData(self, editor, index):
        value = index.model().data(index, Qt.EditRole)
        editor.setValue(int(value))

    def updateEditorGeometry(self, editor, options, index):
        editor.setGeometry(options.rect)

    def setModelData(self, editor, model, index):
        value = str(editor.value())
        model.setData(index, value, Qt.EditRole)


class MyWin(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_CalculatorForm()
        self.ui.setupUi(self)
        self.addToolBar(NavigationToolbar(self.ui.MplWidget.canvas, self))

        self.ui.tableView.horizontalHeader().setSectionResizeMode(1)
        self.ui.tableView.setSelectionMode(1)


        r = np.arange(0,12,dtype=int).reshape(12,1)

        self.table = self.ui.tableView
        self.model = NpModel(r)
        self.table.setModel(self.model)
        self.table.setItemDelegateForColumn(0,SpinDelegate())


        def f(r): return r
        a=list(map(f,r))
        print(a)

        self.ui.MplWidget.canvas.axes.plot(a, c="red", marker='o')
        self.ui.MplWidget.canvas.axes.set_rlim(0.0, 12)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = MyWin()
    myapp.show()
    sys.exit(app.exec())

Ответы (1 шт):

Автор решения: S. Nick

я отметил строки, в которые внес изменения

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (QApplication, QWidget, QTableView, QPushButton,
        QStyledItemDelegate,QSpinBox, QMenu, QVBoxLayout)
from PyQt5.QtGui import QFont,QColor
from PyQt5.QtCore import (Qt, QAbstractTableModel, QModelIndex, QAbstractItemModel)
import numpy as np

from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar     # +   
from matplotlib.figure import Figure


class Ui_CalculatorForm(object):
    def setupUi(self, CalculatorForm):
        CalculatorForm.setObjectName("CalculatorForm")
        CalculatorForm.setWindowModality(QtCore.Qt.NonModal)
        CalculatorForm.resize(615, 460)
        CalculatorForm.setMinimumSize(QtCore.QSize(420, 460))
        palette = QtGui.QPalette()
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
        brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
        brush = QtGui.QBrush(QtGui.QColor(231, 231, 231))
        brush.setStyle(QtCore.Qt.SolidPattern)
        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
        CalculatorForm.setPalette(palette)
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(10)
        CalculatorForm.setFont(font)
        CalculatorForm.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
        CalculatorForm.setFocusPolicy(QtCore.Qt.NoFocus)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("../../ОБР_ТРЕУГ/повтор/Новая папка/1.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        CalculatorForm.setWindowIcon(icon)
        CalculatorForm.setToolTipDuration(-3)
        CalculatorForm.setAutoFillBackground(True)
        CalculatorForm.setInputMethodHints(QtCore.Qt.ImhNone)
        CalculatorForm.setIconSize(QtCore.QSize(40, 24))
        self.centralwidget = QtWidgets.QWidget(CalculatorForm)
        self.centralwidget.setObjectName("centralwidget")
        self.layoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.layoutWidget.setGeometry(QtCore.QRect(0, 0, 2, 2))
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.layoutWidget1 = QtWidgets.QWidget(self.centralwidget)
        self.layoutWidget1.setGeometry(QtCore.QRect(0, 0, 2, 2))
        self.layoutWidget1.setObjectName("layoutWidget1")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget1)
        self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.tableView = QtWidgets.QTableView(self.centralwidget)
        self.tableView.setGeometry(QtCore.QRect(60, 40, 121, 331))
        self.tableView.setEditTriggers(QtWidgets.QAbstractItemView.AnyKeyPressed|QtWidgets.QAbstractItemView.CurrentChanged|QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed)
        self.tableView.setAlternatingRowColors(True)
        self.tableView.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
        self.tableView.setObjectName("tableView")
        self.MplWidget = MplWidget(self.centralwidget)
        self.MplWidget.setGeometry(QtCore.QRect(210, 20, 351, 341))
        self.MplWidget.setObjectName("MplWidget")
        CalculatorForm.setCentralWidget(self.centralwidget)

        self.retranslateUi(CalculatorForm)
        QtCore.QMetaObject.connectSlotsByName(CalculatorForm)

    def retranslateUi(self, CalculatorForm):
        _translate = QtCore.QCoreApplication.translate
        CalculatorForm.setWindowTitle(_translate("CalculatorForm", "Метод треугольника-обратный расчет"))


class MplWidget(QWidget):
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)
        self.canvas = FigureCanvas(Figure())
        vertical_layout = QVBoxLayout()
        vertical_layout.addWidget(self.canvas)
        self.canvas.axes = self.canvas.figure.add_subplot(111, polar = True)
        self.setLayout(vertical_layout)
        self.canvas.axes.set_theta_offset(3.14/2.0)
        self.canvas.axes.set_theta_direction(-1)


class NpModel(QAbstractTableModel):
    def __init__(self, data = np.array([[]]), parent=None):
        super().__init__()
        self.npdata = data

    def rowCount(self,index=QModelIndex()):
        return len(self.npdata)

    def columnCount(self,index=QModelIndex()):
        return len(self.npdata[0])

    def data(self,index,role):
        if not index.isValid(): return None
        row = index.row()
        col = index.column()
        val = self.npdata[row, col]

        if role == Qt.DisplayRole or role == Qt.EditRole:
            return str(round(val,3))
        elif role == Qt.BackgroundRole:
            if col == 0: return QColor(0,0,0,8)

    def flags(self,index):
        if not index.isValid(): return Qt.ItemIsEnabled
        if index.column() == 0:
            return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
        return Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self,index,value,role):
        if not index.isValid():
             return False
        if role == Qt.EditRole:
            try : f = float(value.replace(',','.'))
            except: return False
            self.npdata[index.row(), index.column()] = f
            self.dataChanged.emit(index,index)
            return True
        return False

    def headerData(self,section,orientation,role):
        if role != Qt.DisplayRole: return None
        if orientation == Qt.Horizontal:
            return 'TF' + str(section + 1)
        else: return str(section + 1)

    def load(self,arr):
        self.beginResetModel()
        self.npdata = arr
        self.endResetModel()


class SpinDelegate(QStyledItemDelegate):
    signal_delegate = QtCore.pyqtSignal(int, int)                                               # +++ !!!

    def createEditor(self, parent, options, index):
        editor = QSpinBox(parent)
        editor.valueChanged.connect(lambda val, row=index.row(): self.update_editor(val, row))  # +++                   # +++
        editor.setFrame(False)
        editor.setRange(0, 360)
        editor.setSingleStep(1)
        return editor

    def setEditorData(self, editor, index):
        value = index.model().data(index, Qt.EditRole)
        editor.setValue(int(value))

    def updateEditorGeometry(self, editor, options, index):
        editor.setGeometry(options.rect)

    def setModelData(self, editor, model, index):
        value = str(editor.value())
        model.setData(index, value, Qt.EditRole)

    def update_editor(self, val, row):                                                         # +++
        self.signal_delegate.emit(val, row)                                                    # +++

class MyWin(QtWidgets.QMainWindow, Ui_CalculatorForm):                  # + Ui_CalculatorForm
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
#        self.ui = Ui_CalculatorForm()                                   # ui.
        self.setupUi(self)                                               # ui.

#        self.spinBox = QSpinBox()

        self.addToolBar(NavigationToolbar(self.MplWidget.canvas, self))  # ui.
        self.tableView.horizontalHeader().setSectionResizeMode(1)        # ui.
        self.tableView.setSelectionMode(1)                               # ui.


#+        r = np.arange(0,12,dtype=int).reshape(12,1)
        self.r = np.arange(0, 12, dtype=int).reshape(12, 1)                                  # + self.r

        self.table = self.tableView                                      # ui.
        self.model = NpModel(self.r)
        self.table.setModel(self.model)

        delegate = SpinDelegate(self)                                                   # +
        delegate.signal_delegate.connect(self.update_canvas)                            # +++ !!!

#        self.table.setItemDelegateForColumn(0, SpinDelegate())
        self.table.setItemDelegateForColumn(0, delegate)                                # +++

        def f(r): 
            return r
        a = list(map(f, self.r))
        print(a)
        self.MplWidget.canvas.axes.plot(a, c="red", marker='o') 
        self.MplWidget.canvas.axes.set_rlim(0.0, 12)            

    def update_canvas(self, val, row):                                                  # +++
        #print(f'update_canvas ===+++ {val}, {row}')
        def f(r): 
            return r
        self.r[row] = val                                                               # <--- !!!
        a = list(map(f, self.r))

        self.MplWidget.canvas.axes.clear()
        self.MplWidget.canvas.axes.set_theta_offset(3.14/2.0)
        self.MplWidget.canvas.axes.set_theta_direction(-1)    
        self.MplWidget.canvas.axes.plot(a, c="red", marker='o')    
        self.MplWidget.canvas.axes.set_rlim(0.0, 12)               
        self.MplWidget.canvas.draw()                                                    # !!!


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    myapp = MyWin()
    myapp.show()
    sys.exit(app.exec())

введите сюда описание изображения

→ Ссылка