Osgiliath
Osgiliath

Reputation: 21

QGraphicsView fitInView having very small viewport rect dimension

This minimal code example works well when loading the image using the button:

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


img_path = r'C:\somepath\test.jpg'
ICON_SIZE = 100
ELLIPSE_RADIUS = 35


def pt_distance(pt1, pt2):
    return np.sqrt(np.square(pt2[0]-pt1[0]) + np.square(pt2[1]-pt1[1]))


class PhotoViewer(QtWidgets.QGraphicsView):
    photoClicked = QtCore.pyqtSignal(QtCore.QPoint)

    def __init__(self, parent):
        super().__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(50, 50, 50)))
        self.setFrameShape(QtWidgets.QFrame.NoFrame)
    
    def hasPhoto(self):
        return not self._empty

    def fitInView(self, scale=True):
        rect = QtCore.QRectF(self._photo.pixmap().rect())  # return pixmap enclosing rectangle
        if not rect.isNull():
            self.setSceneRect(rect)  # set the visualized area rectangle to the pixmap's enclosing rectangle
            if self.hasPhoto():
                unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
                self.scale(1 / unity.width(), 1 / unity.height())
                viewrect = self.viewport().rect()
                scenerect = self.transform().mapRect(rect)
                factor = min(viewrect.width() / scenerect.width(),
                             viewrect.height() / scenerect.height())
                self.scale(factor, factor)

    def setPhoto(self, pixmap):
        self._scene.removeItem(self._photo)
        self._zoom = 0
        if pixmap and not pixmap.isNull():
            self._empty = False
            self._photo.setPixmap(pixmap)
            self._scene.addItem(self._photo)
        else:
            self._empty = True
            self._photo.setPixmap(QtGui.QPixmap())
        self.fitInView()

    def mousePressEvent(self, event):
        if self._photo.isUnderMouse():
            self.photoClicked.emit(self.mapToScene(event.pos()).toPoint())
        super().mousePressEvent(event)


class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.viewer = PhotoViewer(self)
        # Styling
        self.setWindowIcon(QtGui.QIcon("pyHelios.png"))
        self.setWindowTitle("pyHelios")
        # 'Load image' button
        self.btnLoad = QtWidgets.QToolButton(self)
        self.btnLoad.setText('Load image')
        self.btnLoad.clicked.connect(self.loadImage)
        self.viewer.photoClicked.connect(self.photoClicked)
        # Arrange layout        
        VBlayout = QtWidgets.QVBoxLayout(self)
        VBlayout.addWidget(self.viewer)
        HBlayout = QtWidgets.QHBoxLayout()
        HBlayout.setAlignment(QtCore.Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)
        VBlayout.addLayout(HBlayout)

    def loadImage(self):
        pixmap = QtGui.QPixmap(img_path)
        self.viewer.setPhoto(pixmap)
        self.viewer._pixmap = pixmap

    def photoClicked(self, pos):
        pt = (pos.x(), pos.y())


app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 1600, 900)
window.show()
app.exec()

However, if I try to load the image without using the button by adding the loadImage() code into the __init__ of the class Window, the fitInView function no longer work as intended (the displayed image is really small:

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.viewer = PhotoViewer(self)
        # Styling
        self.setWindowIcon(QtGui.QIcon("pyHelios.png"))
        self.setWindowTitle("pyHelios")
        # 'Load image' button
        self.btnLoad = QtWidgets.QToolButton(self)
        self.btnLoad.setText('Load image')
        self.btnLoad.clicked.connect(self.loadImage)
        self.viewer.photoClicked.connect(self.photoClicked)
        # Arrange layout
        VBlayout = QtWidgets.QVBoxLayout(self)
        VBlayout.addWidget(self.viewer)
        HBlayout = QtWidgets.QHBoxLayout()
        HBlayout.setAlignment(QtCore.Qt.AlignLeft)
        HBlayout.addWidget(self.btnLoad)
        VBlayout.addLayout(HBlayout)
        pixmap = QtGui.QPixmap(img_path)
->      self.viewer.setPhoto(pixmap)
->      self.viewer._pixmap = pixmap

I dug a bit on what's going on and I basically have viewrect.width() & viewrect.height() super small (98 and 28) instead of (1578, 853). Pixmap rect is 1936x1936.

I am really clueless what I am missing here. I'm guessing that some "hidden" function is getting called when loading the image using the button instead of inside of the class initialization.

Upvotes: 2

Views: 532

Answers (1)

musicamante
musicamante

Reputation: 48231

When a widget is being constructed it always has a default size:

  • if it has no parent, it's 640x480;
  • if it's constructed with a parent, it's 100x30;

Since you're trying to set the view based on the current size, and at that point the widget has not been fully constructed/mapped, nor resized (you're setting the geometry after creating it), it's considering the default size above: you created PhotoViewer with a parent, so it will be 100x30.

A possible solution would be to call your fitInView implementation when it's being shown the first time:

class PhotoViewer(QtWidgets.QGraphicsView):
    shown = False
    # ...
    def showEvent(self, event):
        super().showEvent(event)
        if not self.shown:
            self.shown = True
            self.fitInView()

Upvotes: 2

Related Questions