2
votes

I am trying to create QScrollArea with QHBoxLayout inside containing a row od QWidgets. I want the widgets to occupy all available height and adjust it's size accordingly.

My widgets are QLabels displaying scaled-to-size QPixmaps inside. In my project those are photos. I implemented heightForWidth and sizeHint methods to achieve good scaling. Still I'm unable to achieve correct height.

By default widgets are scaled so that scrollarea does not need scrollbars.

No scrollbars needed

I played with sizePolicy of the scroll widget but was only able to force widgets to have its size equal to its maximum size returned from sizeHint. Horizontal scroll is active then.

Maximum size of widgets exceeds window height

When I change sizeHint to return QSize(width(), heightForWidth(width())) I observe that width() returns up to 640 pixels and never more. I could not figure out where this limit of 640 comes from.

Is there a way to remove the 640px width limit in my layout or is there any way to force widgets to scale up to window height?

class ImageWidget(QLabel):
    PIXMAP_SIZE = QSize(1000, 500)

    def __init__(self, parent=None):
        super(ImageWidget, self).__init__(parent)

        self._imagePixmap = QtGui.QPixmap(self.PIXMAP_SIZE)
        self._imagePixmap.fill(QtGui.QColor(Qt.green))

        self.setScaledContents(False)
        self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.setStyleSheet("border:1px solid red;")

    def paintEvent(self, event):
        size = self.size()
        point = QPoint(0, 0)
        scaledPix = self._imagePixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        point.setX((size.width() - scaledPix.width())/2)
        point.setY((size.height() - scaledPix.height())/2)
        QtGui.QPainter(self).drawPixmap(point, scaledPix)

    def heightForWidth(self, width):
        return self._imagePixmap.height() * width / self._imagePixmap.width() if self._imagePixmap.width() else 0

    def hasHeightForWidth(self):
        return self._imagePixmap is not None

    def sizeHint(self):
        return QSize(self.width(), self.heightForWidth(self.width()))
        # self.width() has max value of 640 !
        #return self.PIXMAP_SIZE


class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.seriesLayout = QHBoxLayout()

        scrollWidget = QWidget()
        scrollWidget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum)
        scrollWidget.setLayout(self.seriesLayout)

        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setWidget(scrollWidget)

        self.setCentralWidget(scroll)

        for col in range(5):
            self.seriesLayout.addWidget(ImageWidget())
1
You will have to resize the widget inside of the scroll area according to the size of the scroll area's client area yourself. You won't need any size hints: the widgets have a fixed size in all cases, with the vertical size identical to the height of the client scroll area, and the horizontal size a fixed ratio of the vertical size. This answer shows how to do some of it.Kuba hasn't forgotten Monica
I'm most puzzled by the fact that the layout forces widgets inside to a maximum size of 640x480. I searched Qt repo and found that the default geometry of a widget is exactly that size. link. Setting new geometry to a max size of my pixmap does not constrain the widgets. Still they overflow available height of the window so vertical scrollbar is visible. I'm suprised that the default settings allow window's content to grow up to window's width, but transposing this behaviour to height is not that simple.piro91
That's expected because there's nothing that constrains the size of the content widget. QScrollArea doesn't do any of it. You must resize the widget yourself, otherwise none of your expectations can even begin to be upheld.Kuba hasn't forgotten Monica

1 Answers

0
votes

I ended up following the comment and added custom resize event for my scrollarea. I used viewport().height() as a preferred height and calculated needed width asking each widget about preferred widthForHeight(). This of course works only with my custom widgets that have this method implemented but I'm sure can be extended to fall back to sizeHint().

Only horizontal scroll active. Widgets adjusted.

Anyway, here is the code:

class MyScrollArea(QScrollArea):
    def __init__(self, parent=None):
        super(MyScrollArea, self).__init__(parent)
        self.setWidgetResizable(True)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)

        self.layout = QHBoxLayout()
        self.layout.setSpacing(30)

        scroll = QWidget()
        scroll.setLayout(self.layout)
        self.setWidget(scroll)

    def eventFilter(self, obj, event):
        if obj == self.widget() and event.type() == QEvent.Resize:
            self.widget().resize(self.calcViewportSize())
            return True

        return super(MyScrollArea, self).eventFilter(obj, event)

    def calcViewportSize(self):
        height = self.viewport().height()

        layoutMargins = self.layout.contentsMargins()
        heightForCalc = height - layoutMargins.top() - layoutMargins.bottom()

        width = self.calcWidthForHeight(heightForCalc)
        return QSize(width, height)

    def calcWidthForHeight(self, height):
        sum = 0
        for wgt in range(self.layout.count()):
            sum += self.layout.itemAt(wgt).widget().widthForHeight(height)

        if self.layout.count() > 1:
            sum += self.layout.spacing() * (self.layout.count()-1)
        return sum

class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.scroll = MyScrollArea()
        self.setCentralWidget(self.scroll)

        for col in range(3):
            self.scroll.layout.addWidget(ImageWidget())