c++qtqscrollareaqscrollbar

Autoscroll QScrollArea when its widget is resized


I have a QScrollArea that contains a widget that can be dynamically expanded down by adding new elements to its layout. I want to automatically scroll the QScrollArea to the bottom when a new element is added to layout.

// widget.hpp

class MainWidget: public QScrollArea {
    Q_OBJECT 
public:
    explicit MainWidget();

public slots:
    void add_new_elem(bool);

private:
    void initialize_new_item(void);

    QWidget* scrolled_widget;
    std::unique_ptr<QVBoxLayout> scrolled_widget_layout;
    std::unique_ptr<QPushButton> add_new_item_button;
};
// widget.cpp
#include "widget.hpp"

MainWidget::MainWidget() : QScrollArea() {
    scrolled_widget = new QWidget(this);
    scrolled_widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    scrolled_widget_layout = std::make_unique<QVBoxLayout>(scrolled_widget);

    add_new_item_button = std::make_unique<QPushButton>("Add new item!");
    add_new_item_button->show();

    connect(add_new_item_button.get(), &QPushButton::clicked, this, &MainWidget::add_new_elem);

    setWidget(scrolled_widget);
    setWidgetResizable(true); // required!
    setAlignment(Qt::AlignBottom | Qt::AlignLeft);
}

void MainWidget::initialize_new_item(void) {
    auto elem = new Elem();
    scrolled_widget_layout->addWidget(elem);
}

void MainWidget::add_new_elem(bool) {
    initialize_new_item();
    QScrollBar* scrollbar = verticalScrollBar();

    scrollbar->setSliderPosition(scrollbar->maximum()); */
}

The issue is that, by the time scrollbar->maximum() is called, the maximum property has not yet been updated (maybe because of the event loop), and the scrollbar is set to an incorrect position.

I searched documentation but did not find signals that indicate geometry changes, and I hate the idea of setting a timer.

How can I make this work?


Solution

  • Use QAbstractSlider::rangeChanged, but take into account that it will get triggered by any size change(*). In this case, I used a simple bool to switch on autoscrolling when an item is added, and switch it off when the scrolling is done:

    #include <QApplication>
    #include <QtWidgets>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        QScrollArea scrollArea;
        QWidget scrollAreaWidget;
        QVBoxLayout scrollAreaWidgetLayout(&scrollAreaWidget);
        QPushButton button("New item");
        bool autoScroll = false;
    
        QObject::connect(&button, &QPushButton::clicked, [&scrollAreaWidgetLayout, &autoScroll]()
        {
            scrollAreaWidgetLayout.addWidget(new QPushButton);
            //enable autoscrolling when an item is added
            autoScroll = true;
        });
        QObject::connect(scrollArea.verticalScrollBar(), &QScrollBar::rangeChanged, [&scrollArea, &autoScroll]()
        {
            if(autoScroll)
            {
                QScrollBar* scrollbar = scrollArea.verticalScrollBar();
                scrollbar->setSliderPosition(scrollbar->maximum());
                //disable it when scrolling is done
                autoScroll = false;
            }
        });
    
        scrollArea.setWidget(&scrollAreaWidget);
        scrollArea.setWidgetResizable(true);
    
        scrollArea.show();
        button.show();
    
        return a.exec();
    }
    

    Scroll area auto scrolled to the bottom when new items are added to it


    (*): Credits to musicamante for pointing this out.