1
votes

I've created a small app and I'm trying to make it so that when the main window is resized (and the GraphicsView and scene are too) that the whole scene (pixmap and rectangles) scale vertically to fit completely inside the GraphicsView. I don't want a vertical scrollbar and I don't want it to scale horizontally.

I can't figure out how to scale the scene properly. I use a GraphicsScene to contain a graph and a couple vertical rectangle "markers". When I can scale the graph to fit by redrawing the pixmap and then reattach it, the z-order is wrong AND the rectangle widgets are not scaled with it.

I need to keep track of the rectangle widgets, so I can't just keep deleting and re-adding them as there's meta data along with each one.

I know about fitInView (from here: Issue with fitInView of QGraphicsView when ItemIgnoresTransformations is on) that applies to the containing GraphicsView, but I don't understand why it needs a parameter. I just want the scene to fit in the GraphicsView (vertically but not horizontally) so why doesn't GraphicsView just scale everything in the scene to fit inside it's current size? What should the parameter look like to get the scene to fit vertically?

In the resizeEvent I can redraw the pixmap and re-add, but then it covers the rectangles as the z-order is messed up. Also, it doesn't stay centered vertically in the scene and I would need to copy over the meta data.

import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
import PyQt5 as qt
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QGroupBox, QDialog, QVBoxLayout
from PyQt5.QtWidgets import QVBoxLayout, QGridLayout, QStackedWidget, QTabWidget
import numpy as np

class GraphicsScene(QtWidgets.QGraphicsScene):
    def __init__(self, parent=None):
        super(GraphicsScene, self).__init__(parent)

    def minimumSizeHint(self):
        return QtCore.QSize(300, 200)

    def dragMoveEvent(self, event):
        print("dragMoveEvent", event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        #super(MainWindow).__init__()

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)

        max_x, max_y = 2400, 700
        max_x_view = 1200
        self.max_x = max_x
        self.max_y = max_y
        self.first = True
        self.setGeometry(200, 200, max_x_view, self.max_y)

        self.gv = QtWidgets.QGraphicsView(self)
        self.gv.setGeometry(0, 0, max_x_view, self.max_y)
        self.gv2 = QtWidgets.QGraphicsView(self)

        layout.addWidget(self.gv)
        layout.addWidget(self.gv2)

        scene = GraphicsScene()
        self.scene = scene

        self.gv.setScene(scene)
        tab_widget = QTabWidget()
        tab_widget.setTabPosition(QTabWidget.West)
        widget = QWidget()
        widget.setLayout(layout)
        tab_widget.addTab(widget, "main")

        self.setCentralWidget(tab_widget)
        np.random.seed(777)
        self.x_time = np.linspace(0, 12.56, 3000)
        rand_data = np.random.uniform(0.0, 1.0, 3000)
        self.data = .45*(np.sin(2*self.x_time) + rand_data) - .25*(np.sin(3*self.x_time))
        self.first = True

        pixmap_height = max_y//2 - 2*22  # 22 to take care of scrollbar height
        pixmap = self.draw_graph()

        pen = QtGui.QPen()
        pen.setWidth(2)
        pen.setColor(QtGui.QColor("red"))
        self.gv1_pixmap = scene.addPixmap(pixmap)
        rect = scene.sceneRect()
        print("scene rect = {}".format(rect))
        scene.setSceneRect(rect)
        side, offset = 50, 200

        for i in range(2):
            r = QtCore.QRectF(QtCore.QPointF((i + 1)*offset + i * 2 * side, 2), QtCore.QSizeF(side, pixmap_height - 4))
            rect_ref = scene.addRect(r, pen, QColor(255, 0, 0, 127))
            rect_ref.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)

        all_items = scene.items()
        print(all_items)

    def draw_graph(self):
        print("draw_graph: main Window size {}:".format(self.size()))

        pixmap_height = self.height()//2 - 2*22  # 22 to take care of scrollbar height
        x_final = self.x_time[-1]
        data = self.data / np.max(np.abs(self.data))
        data = [abs(int(k * pixmap_height)) for k in self.data]
        x_pos = [int(self.x_time[i] * self.max_x / x_final) for i in range(len(data))]

        pixmap = QtGui.QPixmap(self.max_x, pixmap_height)
        painter = QtGui.QPainter(pixmap)
        pen = QtGui.QPen()
        pen.setWidth(2)
        rect = pixmap.rect()
        pen.setColor(QtGui.QColor("red"))
        painter.drawRect(rect)
        print("pixmap rect = {}".format(rect))
        painter.fillRect(rect, QtGui.QColor('lightblue'))
        pen.setWidth(2)
        pen.setColor(QtGui.QColor("green"))
        painter.setPen(pen)
        for x, y in zip(x_pos, data):
            painter.drawLine(x, pixmap_height, x, pixmap_height - y)
        painter.end()
        return pixmap

    def resizeEvent(self, a0: QtGui.QResizeEvent):
        #print("main Window resizeEvent")
        print("main Window  size {}:".format(a0.size()))

        redraw = False
        if redraw:
            pixmap = self.draw_graph()
            self.scene.removeItem(self.gv1_pixmap)
            self.gv1_pixmap = self.scene.addPixmap(pixmap)
            self.gv1_pixmap.moveBy(0, 30)
        else:
            #rect = QtCore.QRect(self.gv.startPos, self.gv.endPos)
            #sceneRect = self.gv.mapToScene(rect).boundingRect()
            #print 'Selected area: viewport coordinate:', rect,', scene coordinate:', sceneRect
            #self.gv.fitInView(sceneRect)
            pass

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
1
Do these rectangles have special requeriments? I suspect that you want these rectangles to take the height of the viewport of the QGraphicsView and that it cannot be moved vertically but only horizontally, am I correct?eyllanesc
That's correct. The rectangles need to be the same height as the pixmap. The pixmap is a little shorter than the viewport because the horizontal scrollbar keeps the pixmap from fitting completely.Gene
Should rectangles be able to move vertically or only horizontally?eyllanesc
No, the rectangles should only move horizontally. Think of them as highlighting sections of the graph.Gene

1 Answers

2
votes

My solution will fit the height of the smallest rectangle that encapsulates all the items (sceneRect) to the viewport of the QGraphicsView. So set the height of the items a value not so small so that the image quality is not lost. I have also scaled the items using the QTransforms. In addition, the QGraphicsView coordinate system was inverted since by default the vertical axis is top-bottom and I have inverted it so that the painting is more consistent with the data.

I have refactored the OP code to make it more scalable, there is a GraphItem that takes the data (x, y) and the image dimensions.

Considering the above, the solution is:

import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets


class GraphItem(QtWidgets.QGraphicsPixmapItem):
    def __init__(self, xdata, ydata, width, height, parent=None):
        super(GraphItem, self).__init__(parent)

        self._xdata = xdata
        self._ydata = ydata
        self._size = QtCore.QSize(width, height)
        self.redraw()

    def redraw(self):
        x_final = self._xdata[-1]
        pixmap = QtGui.QPixmap(self._size)
        pixmap_height = pixmap.height()
        pixmap.fill(QtGui.QColor("lightblue"))
        painter = QtGui.QPainter(pixmap)

        pen = QtGui.QPen(QtGui.QColor("green"))
        pen.setWidth(2)
        painter.setPen(pen)
        for i, (x, y) in enumerate(
            zip(self._xdata, self._ydata / np.max(np.abs(self._ydata)))
        ):
            x_pos = int(x * self._size.width() / x_final)
            y_pos = abs(int(y * pixmap_height))
            painter.drawLine(x_pos, 0, x_pos, y_pos)

        painter.end()
        self.setPixmap(pixmap)


class HorizontalRectItem(QtWidgets.QGraphicsRectItem):
    def itemChange(self, change, value):
        if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.scene():
            newPos = self.pos()
            newPos.setX(value.x())
            return newPos
        return super(HorizontalRectItem, self).itemChange(change, value)


class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super(GraphicsView, self).__init__(parent)
        scene = QtWidgets.QGraphicsScene(self)
        self.setScene(scene)
        self.scale(1, -1)

    def resizeEvent(self, event):

        h = self.mapToScene(self.viewport().rect()).boundingRect().height()
        r = self.sceneRect()
        r.setHeight(h)
        self.setSceneRect(r)

        height = self.viewport().height()
        for item in self.items():
            item_height = item.boundingRect().height()
            tr = QtGui.QTransform()
            tr.scale(1, height / item_height)
            item.setTransform(tr)

        super(GraphicsView, self).resizeEvent(event)

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        tab_widget = QtWidgets.QTabWidget(tabPosition=QtWidgets.QTabWidget.West)
        self.setCentralWidget(tab_widget)

        self.graphics_view_top = GraphicsView()
        self.graphics_view_bottom = QtWidgets.QGraphicsView()

        container = QtWidgets.QWidget()
        lay = QtWidgets.QVBoxLayout(container)
        lay.addWidget(self.graphics_view_top)
        lay.addWidget(self.graphics_view_bottom)

        tab_widget.addTab(container, "main")

        self.resize(640, 480)

        side, offset, height = 50, 200, 400

        np.random.seed(777)
        x_time = np.linspace(0, 12.56, 3000)
        rand_data = np.random.uniform(0.0, 1.0, 3000)
        data = 0.45 * (np.sin(2 * x_time) + rand_data) - 0.25 * (np.sin(3 * x_time))

        graph_item = GraphItem(x_time, data, 3000, height)
        self.graphics_view_top.scene().addItem(graph_item)

        for i in range(2):
            r = QtCore.QRectF(
                QtCore.QPointF((i + 1) * offset + i * 2 * side, 2),
                QtCore.QSizeF(side, height),
            )
            it = HorizontalRectItem(r)
            it.setPen(QtGui.QPen(QtGui.QColor("red"), 2))
            it.setBrush(QtGui.QColor(255, 0, 0, 127))
            self.graphics_view_top.scene().addItem(it)
            it.setFlags(
                it.flags()
                | QtWidgets.QGraphicsItem.ItemIsMovable
                | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
            )


def main():
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()

    ret = app.exec_()

    sys.exit(ret)


if __name__ == "__main__":
    main()