Search code examples
pythonqgraphicsviewqgraphicsscenepyside2qgraphicswidget

How to automatically set rect correctly in QGraphicsView?


I have a couple of different issues that I am trying to solve and I cannot figure out. I have not worked with QGraphicsView etc much before but I created a QGraphicsView that I am trying to load images in and I am trying to use a QSpinBox to dynamically control how many columns that the images use per row. For example if I have 16 images and the QSpinBox is set to 4 "which I have as default" there should be 4 rows and 4 columns of images. That part I seem to have working alright but the code is not very good. I couldn't figure out a way to control how many columns and rows of images there are unless I remove and reload them all each time the count changes. I ended up clearing the the entire scene and reloading everything because I was having trouble removing items individually.

The main issue I am having now is automatically setting the rect for the rows and columns correctly. I am trying to get the scene to resize with the changes without leaving scrollabars with a bunch of empty space.

Here is how it looks at the start when images are loaded.

Images loaded

Here is how it looks after scrolling the QSpinBox a bit. There should only be a vertical scrollbar at this point and not the horizontal scrollbar with a bunch of empty space.

Unnecessary scrollbars

After some trial and error I figured out that in my code I could set the scene rect like this self.scene.setSceneRect(QRectF(self.graphics_widget.rect())) and it seems to work perfectly to fix the scrollbar issue but I can only get it to work when I manually trigger it somehow and for this example that is what the "Set Rect" button does.

Here is an example. After clicking the "Set Rect" button, the vertical scrollbar is correctly removed and the horizontal scrollbar is the exact width of the entire row as it should be.

Proper rect

If I try to set the scene rect automatically after everything has loaded it gives me the wrong width and height but for some reason works properly if I set it manually with a button etc.

The other small issue I am having is getting the red bar on top to stretch further than the viewport area. If I try to scroll horizontally when the horizontal scrollbar is showing it looks like this and I'd like it to stretch the entire width.

Ugly topbar

That is no big deal though, I'm sure I could figure that out. My main concern is getting the scene rect to set automatically so that everything flows correctly without leaving scrollbars with a bunch of empty space.
Does anyone have any suggestions? Thanks.

Here is my code:

from PySide2.QtCore import QRectF, QSizeF, QRect, Qt, QPointF
from PySide2.QtGui import QPixmap, QFont, QColor, QPen, QBrush
from PySide2.QtWidgets import QGraphicsWidget, QWidget, QVBoxLayout, QPushButton, QHBoxLayout, \
    QSpinBox, QGraphicsView, QGraphicsScene, QGraphicsGridLayout, QApplication


class PixmapItem(QGraphicsWidget):
    def __init__(self, image, parent=None):
        super(PixmapItem, self).__init__(parent)
        self.pic = QPixmap(image)
        self._boundingRect = QRectF()
        self.pic_size = self.pic.rect().size()

    def boundingRect(self):
        width = self.pic_size.width()
        height = self.pic_size.height()
        pix_rect = QRectF(0.0, 0.0, width, height)
        self._boundingRect = pix_rect
        return pix_rect

    def sizeHint(self, which, constraint=QSizeF()):
        return self._boundingRect.size()

    def paint(self, painter, option, widget):
        painter.drawPixmap(QRect(0, 40, self.pic_size.width(), self.pic_size.height()), self.pic, self.pic.rect())

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

    def drawBackground(self, painter, rect):
        bg_brush = QBrush(QColor(255, 255, 255), Qt.SolidPattern)
        painter.fillRect(rect, bg_brush)
        #Red bar on top
        painter.setPen(QPen(Qt.NoPen))
        brush = QBrush(QColor(139, 0, 0), Qt.SolidPattern)
        r = QRectF(QPointF(0, 0), QSizeF(rect.width(), 30))
        painter.fillRect(r, brush)
        self.update()

class Widget(QWidget):
    def __init__(self):
        super(Widget, self).__init__()
        self.resize(330, 330)
        self.imglist = ["py.png", "py.png","py.png","py.png","py.png","py.png",
                        "py.png","py.png","py.png","py.png","py.png","py.png"]

        self._layout = QVBoxLayout()
        self._layout2 = QHBoxLayout()

        self.btn = QPushButton("Load")
        self.column_spinbox = QSpinBox()
        self.column_spinbox.setRange(1, 25)
        self.column_spinbox.setValue(4)
        self.column_spinbox.setSingleStep(1)
        self.btn2 = QPushButton("Set Rect")

        self._layout2.addWidget(self.btn)
        self._layout2.addWidget(self.column_spinbox)
        self._layout2.addWidget(self.btn2)
        self._layout.addLayout(self._layout2)

        self._layout3 = QHBoxLayout()
        self.view = QGraphicsView()
        self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.scene = MyScene()
        self.view.setScene(self.scene)
        self.graphics_widget = QGraphicsWidget()
        self.scene.addItem(self.graphics_widget)
        self.graphics_layout = QGraphicsGridLayout()
        self.graphics_widget.setLayout(self.graphics_layout)
        self._layout3.addWidget(self.view)
        self._layout.addLayout(self._layout3)
        self.setLayout(self._layout)
        self.btn.clicked.connect(self._load_pix)
        self.btn2.clicked.connect(self._set_rect)
        self.column_spinbox.valueChanged.connect(self.set_columns)

    def _set_rect(self):
        self.scene.setSceneRect(QRectF(self.graphics_widget.rect()))

    def _load_pix(self):
        for img in range(len(self.imglist)):
            pixmap = PixmapItem(self.imglist[img])
            row, column = divmod(img, self.column_spinbox.value())
            self.graphics_layout.addItem(pixmap, row, column)
            self.graphics_layout.setColumnSpacing(column, 15)
            self.graphics_layout.setRowSpacing(row, 15)
        self.scene.setSceneRect(QRectF(self.graphics_widget.rect()))
        # Add text to top
        item = self.scene.addText("Some Sample Text.", QFont("Arial", 16, QFont.Light))
        item.setDefaultTextColor(QColor(255, 255, 255))

    def set_columns(self):
        self.view.scene().clear()
        #After clearing the scene, re-add the widgets
        self.graphics_widget = QGraphicsWidget()
        self.scene.addItem(self.graphics_widget)
        self.graphics_layout = QGraphicsGridLayout()
        self.graphics_widget.setLayout(self.graphics_layout)
        for img in range(len(self.imglist)):
            pixmap = PixmapItem(self.imglist[img], self.graphics_widget)
            row, column = divmod(img, self.column_spinbox.value())
            self.graphics_layout.addItem(pixmap, row, column)
            self.graphics_layout.setColumnSpacing(column, 15)
            self.graphics_layout.setRowSpacing(row, 15)
        self.scene.setSceneRect(QRectF(self.graphics_widget.rect()))
        #Add text to top again
        item = self.scene.addText("Some Sample Text.", QFont("Arial", 16, QFont.Light))
        item.setDefaultTextColor(QColor(255, 255, 255))

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    app.exec_()

Solution

  • The second problem is caused because the rect you get from drawBackground is about the scene, not the viewport, so the solution is to copy that rectangle and set the height:

    class MyScene(QGraphicsScene):
        def drawBackground(self, painter, rect):
            bg_brush = QBrush(QColor(255, 255, 255), Qt.SolidPattern)
            painter.fillRect(rect, bg_brush)
            #Red bar on top
            painter.setPen(QPen(Qt.NoPen))
            brush = QBrush(QColor(139, 0, 0), Qt.SolidPattern)
            r = QRectF(rect)
            r.setY(0)
            r.setHeight(30)
            painter.fillRect(r, brush)
    

    Still working on the first problem ...

    Complete solution:

    I have accommodated your code since there are many repetitive things but the main task was to implement the adjust_scene method that calculates the viewport using the itemsBoundingRect () and the viewport. The trick is to call that function a moment after setting the new QGraphicsWidget, it is also good to call it in the resizeEvent of QGraphicsView.

    from PySide2 import QtCore, QtGui, QtWidgets
    
    class PixmapItem(QtWidgets.QGraphicsWidget):
        def __init__(self, image, parent=None):
            super(PixmapItem, self).__init__(parent)
            self.pic = QtGui.QPixmap(image)
    
        def boundingRect(self):
            return QtCore.QRectF(self.pic.rect())
    
        def sizeHint(self, which, constraint=QtCore.QSizeF()):
            return self.boundingRect().size()
    
        def paint(self, painter, option, widget):
            painter.drawPixmap(QtCore.QPoint(), self.pic)
    
    class GraphicsScene(QtWidgets.QGraphicsScene):
        def drawBackground(self, painter, rect):
            bg_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255), QtCore.Qt.SolidPattern)
            painter.fillRect(rect, bg_brush)
            #Red bar on top
            painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
            brush = QtGui.QBrush(QtGui.QColor(139, 0, 0), QtCore.Qt.SolidPattern)
            r = QtCore.QRectF(rect)
            r.setY(0)
            r.setHeight(30)
            painter.fillRect(r, brush)
    
    class GraphicsView(QtWidgets.QGraphicsView):
        def adjust_scene(self):
            if self.scene() is None: return
            r = self.scene().itemsBoundingRect()
            view_rect = self.mapToScene(self.viewport().rect()).boundingRect()
            w = max(r.size().width(), view_rect.size().width())
            h = max(r.size().height(), view_rect.size().height())
            self.scene().setSceneRect(QtCore.QRectF(QtCore.QPointF(), QtCore.QSizeF(w, h)))
    
        def resizeEvent(self, event):
            super(GraphicsView, self).resizeEvent(event)
            self.adjust_scene()
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
            self.load_button = QtWidgets.QPushButton(text="Load")
            self.load_button.clicked.connect(self.load)
            self.column_spinbox = QtWidgets.QSpinBox(
                value=4,
                minimum=1,
                maximum=25,
                enabled=False
            )
            self.column_spinbox.valueChanged[int].connect(self.set_columns)
            self.scene = GraphicsScene()
            self.view = GraphicsView(self.scene)
            lay = QtWidgets.QVBoxLayout(self)
            hlay = QtWidgets.QHBoxLayout()
            hlay.addWidget(self.load_button)
            hlay.addWidget(self.column_spinbox)
            lay.addLayout(hlay)
            lay.addWidget(self.view)
    
        @QtCore.Slot()
        def load(self):
            self.imglist = ["py.png", "py.png","py.png","py.png","py.png","py.png",
                "py.png","py.png","py.png","py.png","py.png","py.png"]
            self.set_columns(self.column_spinbox.value())
            self.column_spinbox.setEnabled(True)
    
        @QtCore.Slot(int)
        def set_columns(self, column):
            self.view.scene().clear()
            item = self.scene.addText("Some Sample Text.", QtGui.QFont("Arial", 16, QtGui.QFont.Light))
            item.setDefaultTextColor(QtGui.QColor(255, 255, 255))
    
            top = item.mapToScene(item.boundingRect().bottomLeft())
            self.graphics_widget = QtWidgets.QGraphicsWidget()
            graphics_layout = QtWidgets.QGraphicsGridLayout(self.graphics_widget)
            self.view.scene().addItem(self.graphics_widget)
            self.graphics_widget.setPos(top)
    
            for i, img in enumerate(self.imglist):
                item = PixmapItem(img)
                row, col = divmod(i, column)
                graphics_layout.addItem(item, row, col)
                graphics_layout.setColumnSpacing(column, 15)
                graphics_layout.setRowSpacing(row, 15)
            QtCore.QTimer.singleShot(0, self.view.adjust_scene)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        w = Widget()
        w.show()
        sys.exit(app.exec_())
    

    enter image description here

    enter image description here

    enter image description here

    enter image description here