qtpyqtpyqt5qt5qgraphicsview

QGraphicsView and QSplitter: collapsing and unfolding area does not restore image


I am trying to create a basic program that has two side-by-side panels: one with image, and one with text. I am using splitter to be able to resize both areas up to the point of completely collapsing one of them. This works fine with QTextEdit.

However, when I collapse the QGraphicsView (with additional code that fixes division by zero error), neither the image reappears, nor the background I set as default. I find myself lost at what I should do to make the image and background reappear.

This is the image after loading the image – moving the splitter will also resize the image (this is also the desired final state): Before collapsing the image

This is how it looks after I completely collapse the image and move the splitter back to the center: After collapsing the image

Debug indicates that QPixMap still has the data, but they are not displayed properly. I tried adding self.scene().update(), but that still didn't influence the outcome.

Code:

from PyQt5 import QtCore, QtGui, QtWidgets

class ImageViewer(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super(ImageViewer, self).__init__(parent)
        self._empty = True
        self._scene = QtWidgets.QGraphicsScene(self)
        self._photo = QtWidgets.QGraphicsPixmapItem()
        self._scene.addItem(self._photo)
        self.setScene(self._scene)

        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
        self.setFrameShape(QtWidgets.QFrame.NoFrame)

    def setPhoto(self, pixmap=None):
        if pixmap and not pixmap.isNull():
            self._empty = False
            self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
            self._photo.setPixmap(pixmap)
            self.fitInView()
        else:
            self._empty = True
            self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
            self._photo.setPixmap(QtGui.QPixmap())

    def fitInView(self, scale=True):
        if self._empty:
            return

        rect = QtCore.QRectF(self._photo.pixmap().rect())
        if not rect.isNull():
            self.setSceneRect(rect)
            viewrect = self.viewport().rect()
            scenerect = self.transform().mapRect(rect)
            if scenerect.width() == 0 or scenerect.height() == 0:
                return
            factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height())
            self.scale(factor, factor)

    def resizeEvent(self, event):
        if not self._empty:
            self.fitInView()

class Window(QtWidgets.QMainWindow):
    def __init__(self):
        super(Window, self).__init__()

        self.setWindowTitle("Panel Editor")
        self.setGeometry(250, 150, 1600, 1200)

        self.centralWidget = QtWidgets.QWidget()
        self.setCentralWidget(self.centralWidget)

        self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
        self.viewer = ImageViewer()
        self.splitter.addWidget(self.viewer)

        self.textEditor = QtWidgets.QTextEdit()
        self.splitter.addWidget(self.textEditor)

        # Set the initial sizes of the splitter's panels to divide the space equally
        self.splitter.setSizes([800, 800])

        layout = QtWidgets.QVBoxLayout(self.centralWidget)
        layout.addWidget(self.splitter)

        self.createMenu()

    def createMenu(self):
        self.menuBar = QtWidgets.QMenuBar()
        self.setMenuBar(self.menuBar)
        
        fileMenu = self.menuBar.addMenu('&File')
        openAction = QtWidgets.QAction('&Open', self)
        openAction.setShortcut('Ctrl+O')
        openAction.setStatusTip('Open image')
        openAction.triggered.connect(self.openImage)
        fileMenu.addAction(openAction)

    def openImage(self):
        imagePath, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Open Image", "", "Image Files (*.png *.jpg *.bmp)")
        if imagePath:
            self.viewer.setPhoto(QtGui.QPixmap(imagePath))

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

Solution

  • You're just checking the scenerect, but you're not ensuring that the viewport size is actually valid. The result is that you could end up with a null dimension (width and/or height) of the viewport that will cause the viewrect/scenerect fractions equal to zero, and that will become the factor.

    Since the current factor is also used for self.transform().mapRect(rect), restoring the size of the view will have no effect because the current transform uses the 0 factor above.

    The solution is then to only apply the transform as long as the viewport rectangle is valid (both height and width > 0), which can be done by checking those values or the helper functions isValid() or isNull().

    Alternatively, just set the scale as long as factor > 0.