Search code examples
qtqscrollareaqlayout

How to make a QScrollArea resize to fit its contents


How could I make a QScrollArea stick to the top and grow according to the widgets added to it?

When I add the QScrollArea to the layout like this: vlayout->addWidget(scrollArea, 0, Qt::AlignTop);, it doesn't obey the Qt::AlignTop flag and is floating in the middle:

enter image description here

class Widget : public QWidget  // source: Qt6
{
    Q_OBJECT
public:

    Widget() : QWidget(nullptr)
    {
        QVBoxLayout* vlayout = new QVBoxLayout(this);

        QScrollArea* scrollArea       = new ScrollArea(this);
        QWidget* scrollAreaWidget     = new QWidget(scrollArea);
        QVBoxLayout* scrollAreaLayout = new QVBoxLayout(scrollAreaWidget);
        scrollAreaLayout->setContentsMargins(0, 0, 0, 0);
        scrollAreaLayout->setSizeConstraint(QLayout::SetFixedSize);
        scrollAreaWidget->setLayout(scrollAreaLayout);
        scrollArea->setWidget(scrollAreaWidget);
        scrollArea->setWidgetResizable(true);
        scrollArea->setAlignment(Qt::AlignTop);
        scrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
        //scrollArea->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

        QPushButton* add = new QPushButton("add", this);
        connect(add, &QPushButton::clicked, [=]
        {
            QWidget* widget = new QWidget;
            QHBoxLayout* hlayout = new QHBoxLayout(widget);
            hlayout->setContentsMargins(0, 0, 0, 0);
            QPushButton* button  = new QPushButton("button_" + QString::number(scrollAreaLayout->count()), this);
            button->setFixedWidth(300);
            QPushButton* remove  = new QPushButton("remove", this);
            hlayout->addWidget(button);
            hlayout->addWidget(remove);
            connect(remove, &QPushButton::clicked, [=]{ widget->deleteLater(); });
            scrollAreaLayout->addWidget(widget, 0, Qt::AlignTop);
        });

        vlayout->addWidget(add, 0, Qt::AlignTop);
        //vlayout->addWidget(scrollArea);
        vlayout->addWidget(scrollArea, 0, Qt::AlignTop);
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget widget;
    widget.show();
}

When I add it to the layout this way: vlayout->addWidget(scrollArea);, it grows to the entire layout height:

enter image description here

I have tried all size policies, e.g.:

scrollArea->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

I also tried changing sizeAdjustPolicy:

scrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);

The documentation says:

QAbstractScrollArea::AdjustToContents
The scroll area will always adjust to the viewport

Why is it not adjusting? Am I doing something wrong?

What I'm trying to achieve is to have it on the top and make it grow according to the widgets added to it.

It would look like this:

enter image description here

Then it would continue growing for each widget added until there's no more space on the parent layout, and at this point, it would then display the QScrollBars.


Solution

  • Spacers are better suited to force widgets into a direction, as they act as an excess space consumer, and they are adjustable.

    Also, based on what musicamante said:

    QAbstractScrollArea keeps a cached size hint once it's initialized. If you add child widgets to its contents, then you need to override its ::sizeHint()[...]

    And:

    You need to call updateGeometry() when the size hint of a widget changes.

    So, from the source code of QAbstractScrollArea(1), I borrowed the implementation of sizeHint, and used QScrollArea::widget's sizeHint instead of the viewport's as in the original implementation, but you could also use viewportSizeHint(2).

    Take into account the comments in the following code:

    #include <QApplication>
    #include <QtWidgets>
    
    class ScrollArea : public QScrollArea
    {
    
    public:
        QSize sizeHint() const override
        {
            if (sizeAdjustPolicy() == QAbstractScrollArea::AdjustIgnored)
                return QSize(256, 192);
            //I made a change here where I check for the existence of QScrollArea::widget
            if (widget() && sizeAdjustPolicy() == QAbstractScrollArea::AdjustToContents)
            {
                QSize newSizeHint;
                const int f = 2 * frameWidth();
                const QSize frame(f, f);
                const bool vbarHidden = !verticalScrollBar()->isVisibleTo(this) || verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff;
                const bool hbarHidden = !horizontalScrollBar()->isVisibleTo(this) || horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff;
                const QSize scrollbars(vbarHidden ? 0 : verticalScrollBar()->sizeHint().width(),
                                       hbarHidden ? 0 : horizontalScrollBar()->sizeHint().height());
    
                //here we take into account QScrollArea::widget size hint
                newSizeHint = frame + scrollbars + widget()->sizeHint()/*viewportSizeHint()*/;
                return newSizeHint;
            }
            return QScrollArea::sizeHint();
        }
    };
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        QWidget widget;
        QVBoxLayout* vlayout = new QVBoxLayout(&widget);
    
        ScrollArea scrollArea;
        QWidget scrollAreaWidget(&scrollArea);
        QVBoxLayout scrollAreaLayout(&scrollAreaWidget);
        scrollArea.setWidget(&scrollAreaWidget);
        scrollArea.setWidgetResizable(true);
        //this allows us to get into the section
        //that calculates the scrollArea's size based on its contents in its sizeHint
        scrollArea.setSizeAdjustPolicy(QScrollArea::AdjustToContents);
        //we need to set sizePolicy to Preferred for the height
        //to avoid the scrollarea expanding needlessly when it has no content
        scrollArea.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
        //visuals to demonstrate the size of the scrollarea
        scrollArea.setStyleSheet("QScrollArea{border: 2px solid red}");
    
        QVBoxLayout innerScrollAreaLayout;
        //to avoid using layout-alignment flags
        //use another layout which will contain the added widgets
        //and add a spacer sibling underneath it to push the content upwards
        scrollAreaLayout.addLayout(&innerScrollAreaLayout);
        scrollAreaLayout.addStretch();
    
        QPushButton add("add");
        QObject::connect(&add, &QPushButton::clicked, [&scrollArea, &scrollAreaWidget, &innerScrollAreaLayout]
        {
            QWidget* widget = new QWidget;
            QHBoxLayout* hlayout = new QHBoxLayout(widget);
            hlayout->setContentsMargins(0, 0, 0, 0);
            QPushButton* button  = new QPushButton("button_" + QString::number(innerScrollAreaLayout.count()));
            button->setFixedWidth(300);
            QPushButton* remove  = new QPushButton("remove");
            hlayout->addWidget(button);
            hlayout->addWidget(remove);
            QObject::connect(remove, &QPushButton::clicked, [widget, &scrollArea]
            {
                widget->deleteLater();
                //to make the scrollArea ruduce its size
                //notify it using this
                scrollArea.updateGeometry();
            });
            //add it to the inner layout which has a spacer as a sibling
            //and is contained inside the QScrollArea::widget's layout
            innerScrollAreaLayout.addWidget(widget);
            //this makes sure the layout takes into account the new size hint
            //without it, sizeHint will not be called
            scrollArea.updateGeometry();
        });
    
        vlayout->addWidget(&add);
        vlayout->addWidget(&scrollArea);
        //this is necessary as it does not allow the scrollarea to expand
        //and take more space than it actually needs
        //it basically adds a spacer that takes the space excess
        vlayout->addStretch();
    
        widget.show();
    
        return a.exec();
    }
    

    A GIF displaying an adjustable scroll area being resized automatically when adding/removing widgets to/from it


    (1): note that there is a bug currently in line 1436: const bool hbarHidden = !d->vbar->isVisibleTo(this) || d->hbarpolicy == Qt::ScrollBarAlwaysOff;, where instead of d->hbar, there's d->vbar. I reported this here: QTBUG-123886.
    (2): this does not mean QScrollArea::viewport and QScrollArea::widget are the same, as the first is the "window" we're looking through at the content, and the second is the inner container that expands beyond the viewport when needed.