QScrollArea with dynamically changing contents

GeneralFailure picture GeneralFailure · Jan 21, 2014 · Viewed 7.3k times · Source

I have a QScrollArea with some buttons in it, like shown on the picture. enter image description here

The idea of the layout is: 1. The left and right button should be used for scrolling the buttons when they are too wide

2.The numbers of buttons in the scroll area can be changed dynamically 3. Any free space should be used to expand the scroll area as much as possible. If no such space exist navigation buttons should be used for scrolling.

With my current implementation when i increase the buttons i have this: enter image description here

But there is free space on the right, so this should look like: enter image description here

If i increase once more to 10 for example, then scrollbar should appear( because the layout is constained by the widget ).

I want to know if there is any other way aside from manual resizing of the widgets( because ui can be translated and buttons can change size hint also the real design is more complicated :(

Here is my implementation of the ScrollAreaTest widget:

#include "MainWidget.h"

#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QPushButton>
#include <QDebug>
#include "ButtonWidget.h"

#include "CheckableButtonGroup.h"

MainWidget::MainWidget(QWidget *parent)
    : QWidget(parent),
      m_scrollArea( 0 ),
      m_lineEdit( 0 ),
      m_buttons( 0 )
{
    QVBoxLayout* mainLayout = new QVBoxLayout( this );
    QWidget* firstRow = new QWidget;
    QHBoxLayout* firstRowLayout = new QHBoxLayout( firstRow );

    QPushButton* left  = new QPushButton;
    QPushButton* right = new QPushButton;

    m_buttons = new CheckableButtonGroup( Qt::Horizontal );
    m_buttons->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
    m_buttons->setButtonsCount( 5 );
    m_buttons->setStyleSheet( "border: none" );

    QWidget* const buttonsContainer = new QWidget;
    QHBoxLayout* const buttonsContainerLayout = new QHBoxLayout( buttonsContainer );
    buttonsContainerLayout->setSpacing( 0 );
    buttonsContainerLayout->setSizeConstraint( QLayout::SetMinAndMaxSize );
    buttonsContainerLayout->setMargin( 0 );
    buttonsContainerLayout->addWidget( m_buttons, 0, Qt::AlignLeft );

    qDebug() << m_buttons->buttons()[ 0 ]->size();

    m_scrollArea = new QScrollArea;
    m_scrollArea->setContentsMargins( 0, 0, 0, 0 );
    m_scrollArea->setWidget( buttonsContainer );
    m_scrollArea->setWidgetResizable( true );
    m_scrollArea->setStyleSheet( "border: 1px solid blue" );
    m_scrollArea->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );

    firstRowLayout->addWidget( left        , 0, Qt::AlignLeft );
    firstRowLayout->addWidget( m_scrollArea, 1, Qt::AlignLeft );
    firstRowLayout->addWidget( right       , 0, Qt::AlignLeft );

    m_lineEdit = new QLineEdit;
    QPushButton* button = new QPushButton;
    QHBoxLayout* secondRowLayout = new QHBoxLayout;
    secondRowLayout->addWidget( m_lineEdit );
    secondRowLayout->addWidget( button );

    connect( button, SIGNAL(clicked()), SLOT(setButtonsCount()) );

    mainLayout->addWidget( firstRow, 1, Qt::AlignLeft );
    mainLayout->addLayout( secondRowLayout );

    button->setText( "Set buttons count" );

    buttonsContainer->resize( m_buttons->buttonsOptimalWidth(), buttonsContainer->height() );
    m_buttons->resize( m_buttons->buttonsOptimalWidth(), m_buttons->height() );

    //area->resize( 100, area->height() );
    //area->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
}

MainWidget::~MainWidget()
{
}

void MainWidget::setButtonsCount()
{
    m_buttons->setButtonsCount( m_lineEdit->text().toInt() );
}

And here is the whole Qt project containing the problem: https://drive.google.com/file/d/0B-mc4aKkzWlxQzlPMEVuNVNKQjg/edit?usp=sharing

Answer

The essential steps are:

  1. The container widget that holds the buttons (your CheckableButtonGroup) must have a QLayout::SetMinAndMaxSize size constraint set. Then it will be exactly large enough to hold the buttons. Its size policy doesn't matter, since you're simply putting it into a QScrollArea, not into another layout.

  2. The scroll area needs to set its maximum size according to the size of the widget it holds. The default implementation doesn't do it, so one has to implement it by spying on resize events of the embedded widget.

The code below is a minimal example that works under both Qt 4.8 and 5.2.

Screenshot with two buttons

Screenshot with multiple buttons

// https://github.com/KubaO/stackoverflown/tree/master/questions/scrollgrow-21253755
#include <QtGui>
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#include <QtWidgets>
#endif

class ButtonGroup : public QWidget {
   Q_OBJECT
   QHBoxLayout m_layout{this};
public:
   ButtonGroup(QWidget * parent = 0) : QWidget{parent} {
      m_layout.setSizeConstraint(QLayout::SetMinAndMaxSize); // <<< Essential
   }
   Q_SLOT void addButton() {
      auto n = m_layout.count();
      m_layout.addWidget(new QPushButton{QString{"Btn #%1"}.arg(n+1)});
   }
};

class AdjustingScrollArea : public QScrollArea {
   bool eventFilter(QObject * obj, QEvent * ev) {
      if (obj == widget() && ev->type() == QEvent::Resize) {
         // Essential vvv
         setMaximumWidth(width() - viewport()->width() + widget()->width());
      }
      return QScrollArea::eventFilter(obj, ev);
   }
public:
   AdjustingScrollArea(QWidget * parent = 0) : QScrollArea{parent} {}
   void setWidget(QWidget *w) {
      QScrollArea::setWidget(w);
      // It happens that QScrollArea already filters widget events,
      // but that's an implementation detail that we shouldn't rely on.
      w->installEventFilter(this);
   }
};

class Window : public QWidget {
   QGridLayout         m_layout{this};
   QLabel              m_left{">>"};
   AdjustingScrollArea m_area;
   QLabel              m_right{"<<"};
   QPushButton         m_add{"Add a widget"};
   ButtonGroup         m_group;
public:
   Window() {
      m_layout.addWidget(&m_left, 0, 0);
      m_left.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
      m_left.setStyleSheet("border: 1px solid green");

      m_layout.addWidget(&m_area, 0, 1);
      m_area.setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
      m_area.setStyleSheet("QScrollArea { border: 1px solid blue }");
      m_area.setWidget(&m_group);
      m_layout.setColumnStretch(1, 1);

      m_layout.addWidget(&m_right, 0, 2);
      m_right.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
      m_right.setStyleSheet("border: 1px solid green");

      m_layout.addWidget(&m_add, 1, 0, 1, 3);
      connect(&m_add, SIGNAL(clicked()), &m_group, SLOT(addButton()));
   }
};

int main(int argc, char *argv[])
{
   QApplication a{argc, argv};
   Window w;
   w.show();
   return a.exec();
}

#include "main.moc"