PyQt5 connect scenes from several objects

78 Views Asked by At

On one side I have a tool/class WidgetFigures which lets you draw several figures on a GraphicsView. It does so by subclassing QGraphicsScene and by setting this scene to the GraphicsView object.

On the other side I have a tool/class ChartItem, which lets you plot several PlotDataItem's inside a pyqtgraph.GraphicsLayoutWidget object. The data plotted here is retrieved from an API.

These tools work perfectly fine separately.

My goal is to be able to draw the figures in the first tool on the top of the chart/s of the second tool. I have achieved this functionality as you can see in the following picture.

enter image description here

The problem is that when the plot changes, either when the range changes, or when the window -and therefore the plot- resizes, the figures on top won't move/resize accordingly. This can be seen in the two pictures below, where the figure -oval colored in green- is not highlighting the peak in the plot as the picture before.

enter image description here

enter image description here

Solutions that I've tried:

1 - Use a unique scene: did not work since I am not able to plot PlotDataItems on a subclassed scene. Specifically, axes are not being drawn in the first place, and the plotted data (included axes) can't be seen after.

2 - Detect the plot's ViewBox -or its QGraphicsRectItem- changes, create copies of these items -in the pictures these rects are highlighted in red-, and also apply the transformations -translations and scaling- happening on these items, to each figure added inside the rectangle. I didn't succeed with this one since the transformation applied on the figures is not matching the transformations on the plot's data. The code for this solution is added below.

3 - Instead of creating a QGraphicsRectItem as in solution 2, create some ViewBox, and instead of applying the transformation to each figure individually, just apply the new range to this viewbox. This solution would hide the PlotDataItem under it, I can't make this item transparent.

Here is the code for solution 2:

helper.py


import numpy as np
from PyQt5.QtCore import Qt, QObject, QPointF, pyqtSignal
from PyQt5.QtGui import QPen, QPainter, QTransform
from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, \
    QGraphicsRectItem, QGraphicsPolygonItem, QGraphicsItem
import pyqtgraph as pg


def SMA(v, n):
    i = 0
    moving_averages = []
    moving_averages.extend(v[:n-1])

    while i < len(v) - n + 1:
        window = v[i: i + n]
        window_average = round(np.sum(window) / n, 2)
        moving_averages.append(window_average)
        i += 1
    return moving_averages

class ChartItem:
    plots: List[pg.PlotDataItem]

    def __init__(self, widget: pg.GraphicsLayoutWidget):
        self.setup_widgets(widget)
        self.data = np.random.normal(size=(1, 100), scale=1)
        self.paint(plot=1)

    def setup_widgets(self, widget: pg.GraphicsLayoutWidget):
        p1 = widget.addPlot(0, 0)
        p2 = widget.addPlot(1, 0)
        widget.ci.layout.setRowStretchFactor(0, 2)  # row 0, stretch factor 2
        widget.ci.layout.setRowStretchFactor(1, 1)  # row 1, stretch factor 1
        p2.setXLink(p1)
        # get handle to x-axis 0
        p2.getAxis('bottom').setStyle(showValues=False)
        self.plots = [p1.plot(x=[], y=[]), p2.plot(x=[], y=[])]

    def paint(self, plot: int = 1):
        if plot == 1:
            x = range(50)
            y = self.data[0][:50]
        else:
            x = range(100)
            y = self.data[0]

        self.plots[0].setData(x=x, y=y)
        self.plots[1].setData(x=x, y=SMA(y, 3))


class Widget(pg.GraphicsLayoutWidget):
    sigPainted = pyqtSignal()
    sigResized = pyqtSignal()

    def __init__(self, parent, **kargs):
        super(Widget, self).__init__(parent, **kargs)

    def resizeEvent(self, ev):
        super().resizeEvent(ev)
        self.sigResized.emit()

    def paintEvent(self, ev):
        super().paintEvent(ev)
        if ev.type() == ev.Type.Paint:
            self.sigPainted.emit()
        elif ev.type() != ev.Type.Timer():
            print(ev.type())


class WidgetFigures:
    window: QObject

    def __init__(self, window=None):
        self.window = window
        self.chart = QGraphicsView(window.widget)
        self.chart.setStyleSheet("background: transparent")

        self.chart.setSceneRect(window.widget.sceneRect())
        self.chart.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.chart.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.pHeight = {}
        self.pWidth = {}

        def resizeItem():

            self.chart.setFixedSize(window.widget.size())
            figures = [item for item in self.chart.items() if not isinstance(item, QGraphicsRectItem)]
            widget_rects = [item for item in self.window.widget.items() if isinstance(item, QGraphicsRectItem)]

            for n, rec in enumerate(widget_rects):
                wid = rec.parentItem()
                if hasattr(wid, 'viewRange'):
                    height = wid.scene().height()
                    width = wid.scene().width()
                    fHeight = height / self.pHeight.get(n, height)
                    self.pHeight[n] = height

                    fWidth = width / self.pWidth.get(n, width)
                    self.pWidth[n] = width

                    t = QTransform()
                    t.scale(fWidth, fHeight)
                    for fig in figures:
                        fig.setTransform(t)

        def setRects():
            widget_rects = [item for item in self.window.widget.items() if isinstance(item, QGraphicsRectItem)]
            rects = [item for item in self.chart.items() if isinstance(item, QGraphicsRectItem)]

            if len(rects) == 0:
                rects = []
                for rec in widget_rects:
                    rect = QGraphicsRectItem()
                    rect.setParentItem(rec.parentItem())
                    rect.setPen(QPen(Qt.GlobalColor.red, 2, Qt.SolidLine))
                    rects.append(rect)
                    self.chart.scene().addItem(rect)

            for n, rec in enumerate(widget_rects):
                rects[n].setRect(rec.sceneBoundingRect())

        self.window.widget.sigPainted.connect(setRects)
        self.window.widget.sigResized.connect(resizeItem)
        self.setupScene(self.chart)

        def setSceneRect(rect):
            self.chart.setSceneRect(rect)

        self.window.widget.sceneObj.sceneRectChanged.connect(setSceneRect)

    def setupScene(self, obj):
        sceneRect = self.window.widget.sceneObj.sceneRect()
        self.scene = Scene(obj)
        self.scene.setSceneRect(sceneRect)
        obj.setScene(self.scene)
        obj.setRenderHints(QPainter.Antialiasing)


class Scene(QGraphicsScene):
    numItems: int
    itemToDraw: Optional[QGraphicsPolygonItem]
    parent: QObject
    origPoint: QPointF

    def __init__(self, parent=None):
        QGraphicsScene.__init__(self, parent)
        self.parent = parent
        self.itemToDraw = None

    def makeItemsControllable(self, areControllable: bool):
        for item in self.items():
            item.setFlag(QGraphicsItem.ItemIsSelectable, areControllable)
            item.setFlag(QGraphicsItem.ItemIsMovable, areControllable)

    def mousePressEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
        self.origPoint = event.scenePos()
        self.numItems = len(self.items())
        print("Objects: {}".format(self.numItems))
        super(Scene, self).mousePressEvent(event)

    def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:

        self.itemToDraw = QGraphicsEllipseItem()
        self.itemToDraw.setPen(QPen(Qt.GlobalColor.green, 3, Qt.SolidLine))

        self.itemToDraw.setPos(self.origPoint)
        self.itemToDraw.setRect(0, 0, event.scenePos().x() - self.origPoint.x(),
                                event.scenePos().y() - self.origPoint.y())
        for item in self.items()[: None if not self.numItems else -self.numItems]:
            self.removeItem(item)

        self.addItem(self.itemToDraw)

example.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'example.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1024, 511)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
        MainWindow.setSizePolicy(sizePolicy)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
        self.centralwidget.setSizePolicy(sizePolicy)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout()
        self.verticalLayout_2.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth())
        self.tabWidget.setSizePolicy(sizePolicy)
        self.tabWidget.setAcceptDrops(True)
        self.tabWidget.setToolTip("")
        self.tabWidget.setTabShape(QtWidgets.QTabWidget.Triangular)
        self.tabWidget.setMovable(False)
        self.tabWidget.setObjectName("tabWidget")
        self.tab = QtWidgets.QWidget()
        self.tab.setObjectName("tab")
        self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.tab)
        self.horizontalLayout_10.setObjectName("horizontalLayout_10")
        self.widget = Widget(self.tab)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
        self.widget.setSizePolicy(sizePolicy)
        self.widget.setObjectName("widget")
        self.horizontalLayout_10.addWidget(self.widget)
        self.verticalLayout_4 = QtWidgets.QVBoxLayout()
        self.verticalLayout_4.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
        self.verticalLayout_4.setContentsMargins(5, -1, 5, -1)
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.pushButton = QtWidgets.QPushButton(self.tab)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout_4.addWidget(self.pushButton)
        self.pushButton_2 = QtWidgets.QPushButton(self.tab)
        self.pushButton_2.setObjectName("pushButton_2")
        self.verticalLayout_4.addWidget(self.pushButton_2)
        self.horizontalLayout_10.addLayout(self.verticalLayout_4)
        self.tabWidget.addTab(self.tab, "")
        self.tab_2 = QtWidgets.QWidget()
        self.tab_2.setObjectName("tab_2")
        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tab_2)
        self.verticalLayout_5.setObjectName("verticalLayout_5")
        self.tabWidget.addTab(self.tab_2, "")
        self.tab_5 = QtWidgets.QWidget()
        self.tab_5.setObjectName("tab_5")
        self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.tab_5)
        self.verticalLayout_7.setObjectName("verticalLayout_7")
        self.tabWidget.addTab(self.tab_5, "")
        self.horizontalLayout_3.addWidget(self.tabWidget)
        self.verticalLayout_2.addLayout(self.horizontalLayout_3)
        self.verticalLayout_2.setStretch(0, 5)
        self.verticalLayout_3.addLayout(self.verticalLayout_2)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1024, 31))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.tabWidget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "Plot1"))
        self.pushButton_2.setText(_translate("MainWindow", "Plot2"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("MainWindow", "Tab1"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("MainWindow", "Tab2"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), _translate("MainWindow", "Tab3"))
from helper import Widget, ChartItem, WidgetFigures

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)

    item = ChartItem(widget=ui.widget)

    ui.pushButton.clicked.connect(lambda x: item.paint(1))
    ui.pushButton_2.clicked.connect(lambda x: item.paint(2))

    figures = WidgetFigures(window=ui)
    MainWindow.show()
    sys.exit(app.exec_())

I wonder if there's any way to achieve this functionality. Any help is highly appreciated.

0

There are 0 best solutions below