c++qtqgroupbox

Making collapsible GroupBoxes in Qt - what determines the collapsed size?


I'm trying to make GroupBoxes with a Collapse/Expand button with a hopefully obvious function in Qt. I have subclassed QGroupBox with draws the button. When clicked, my code calls setVisible(false) on all children of the GroupBox. Additionally it goes over all QLayouts in the GroupBox and sets their contentMargins to zero. However, some GroupBoxes still come out larger than others in the collapsed state and I have no good idea what could cause this.

This is what I have come up with so far (I'm aware of the possible nullptr issue):

void CollapsibleGroupBox::onVisibilityChanged()
{
  CollapseExpandButton::State s;

  s = m_clExpButton->state();

  QLayout *master = this->layout();
  QList<QObject *> children = this->children();

  switch (s) {
  case CollapseExpandButton::State::COLLAPSED:
    for (QObject *o : children) {
      QWidget *w = qobject_cast<QWidget *>(o);
      if (w != nullptr) {
        if (w != m_clExpButton)
          w->setVisible(false);

        continue;
      }

      if (o == master) {
        m_layoutMargins.clear();
        collapseLayout(master);
      }
    }
    break;
  case CollapseExpandButton::State::EXPANDED:
    for (QObject *o : children) {
      QWidget *w = qobject_cast<QWidget *>(o);
      if (w != nullptr) {
        w->setVisible(true);

        continue;
      }

      if (o == master)
        expandLayout(master);

    }
    break;
  }
}


void CollapsibleGroupBox::collapseLayout(QLayout *layout)
{
  for (QObject *o : layout->children()) {
    QLayout *l= qobject_cast<QLayout *>(o);
    if (l == nullptr)
      continue;

    collapseLayout(l);
  }
  if (m_layoutMargins.contains(layout))
    return;

  QMargins m = layout->contentsMargins();
  m_layoutMargins[layout] = m;
  layout->setContentsMargins(0, 0, 0, 0);
}

void CollapsibleGroupBox::expandLayout(QLayout *layout)
{
  for (QObject *o : layout->children()) {
    QLayout *l = qobject_cast<QLayout *>(o);
    if (l == nullptr)
      continue;

    if (m_layoutMargins.contains(l))
      expandLayout(l);
  }
  if (m_layoutMargins.contains(layout)) {
    QMargins m = m_layoutMargins[layout];
    layout->setContentsMargins(m);
  }
}

Solution

  • I got back to this old code of mine some time ago and it appears that I finally came up with a reasonable solution. The complete code follows:

    ** CollapsibleGroupBox.h **

    #ifndef COLLAPSIBLEGROUPBOX_H
    #define COLLAPSIBLEGROUPBOX_H
    
    #include <QGroupBox>
    #include <QMap>
    #include <QMargins>
    #include <QPair>
    
    class QResizeEvent;
    class CollapseExpandButton;
    class QSpacerItem;
    
    class CollapsibleGroupBox : public QGroupBox
    {
    public:
      explicit CollapsibleGroupBox(QWidget *parent = nullptr);
    
    protected:
      void resizeEvent(QResizeEvent *);
    
    private:
      void resizeCollapseButton();
      void collapseLayout(QLayout *layout);
      void collapseSpacer(QSpacerItem *spacer);
      void expandLayout(QLayout *layout);
      void expandSpacer(QSpacerItem *spacer);
    
      CollapseExpandButton *m_clExpButton;
      QMap<const void *, QMargins> m_layoutMargins;
      QMap<const void *, QPair<QSize, QSizePolicy>> m_spacerSizes;
    
    private slots:
      void onScreenChanged();
      void onVisibilityChanged();
    
    };
    
    #endif // COLLAPSIBLEGROUPBOX_H
    

    ** CollapsibleGroupBox.cpp **

    #include "collapsiblegroupbox.h"
    #include "collapseexpandbutton.h"
    #include <QApplication>
    #include <QGuiApplication>
    #include <QLayout>
    #include <QResizeEvent>
    #include <QScreen>
    #include <QStyle>
    #include <QTimer>
    #include <QWindow>
    #include <cassert>
    #include <cmath>
    
    inline
    QWindow *findWindowForWidget(const QWidget *widget)
    {
      for (;;) {
        QWindow *wh = widget->window()->windowHandle();
        if (wh != nullptr)
          return wh;
    
        widget = qobject_cast<const QWidget *>(widget->parent());
        if (widget == nullptr)
          return nullptr;
      }
    }
    
    inline
    QScreen * findScreenForWidget(const QWidget *widget)
    {
      for (;;) {
        QWindow *wh = widget->window()->windowHandle();
        if (wh != nullptr) {
          QScreen *scr = wh->screen();
          if (scr != nullptr)
            return scr;
        }
    
        widget = qobject_cast<const QWidget *>(widget->parent());
        if (widget == nullptr)
          return nullptr;
      }
    }
    
    CollapsibleGroupBox::CollapsibleGroupBox(QWidget *parent) :
      QGroupBox(parent)
    {
      m_clExpButton = new CollapseExpandButton(this);
    
      connect(m_clExpButton, &CollapseExpandButton::clicked, this, &CollapsibleGroupBox::onVisibilityChanged);
    
      QTimer::singleShot(0, this, [this] {
        auto wh = findWindowForWidget(this);
        if (wh != nullptr)
          connect(wh, &QWindow::screenChanged, this, &CollapsibleGroupBox::onScreenChanged);
      });
    
      QTimer::singleShot(0, this, &CollapsibleGroupBox::resizeCollapseButton);
    }
    
    void CollapsibleGroupBox::collapseLayout(QLayout *lay)
    {
      assert(!m_layoutMargins.contains(lay));
    
      const int cnt = lay->count();
      for (int idx = 0; idx < cnt; idx++) {
        auto lit = lay->itemAt(idx);
    
        if (lit->widget()) {
          auto w = lit->widget();
          if (w != m_clExpButton)
            w->setVisible(false);
        }
        else if (lit->spacerItem())
          collapseSpacer(lit->spacerItem());
        else if (lit->layout())
          collapseLayout(lit->layout());
      }
    
      m_layoutMargins[lay] = lay->contentsMargins();
      lay->setContentsMargins(0, 0, 0, 0);
    }
    
    void CollapsibleGroupBox::collapseSpacer(QSpacerItem *spacer)
    {
      assert(!m_spacerSizes.contains(spacer));
    
      m_spacerSizes[spacer] = {spacer->sizeHint(), spacer->sizePolicy()};
      spacer->changeSize(0, 0);
    }
    
    void CollapsibleGroupBox::expandLayout(QLayout *lay)
    {
      assert(m_layoutMargins.contains(lay));
    
      const int cnt = lay->count();
      for (int idx = 0; idx < cnt; idx++) {
        auto lit = lay->itemAt(idx);
    
        if (lit->widget())
          lit->widget()->setVisible(true);
        else if (lit->spacerItem())
          expandSpacer(lit->spacerItem());
        else if (lit->layout())
          expandLayout(lit->layout());
      }
    
      lay->setContentsMargins(m_layoutMargins[lay]);
    }
    
    void CollapsibleGroupBox::expandSpacer(QSpacerItem *spacer)
    {
      assert(m_spacerSizes.contains(spacer));
    
      const auto &sz = m_spacerSizes[spacer].first;
      const auto &pol = m_spacerSizes[spacer].second;
    
      spacer->changeSize(sz.width(), sz.height(), pol.horizontalPolicy(), pol.verticalPolicy());
    }
    
    void CollapsibleGroupBox::onScreenChanged()
    {
      resizeCollapseButton();
    }
    
    void CollapsibleGroupBox::onVisibilityChanged()
    {
      assert(this->layout() != nullptr);
    
      CollapseExpandButton::State s = m_clExpButton->state();
    
      switch (s) {
      case CollapseExpandButton::State::COLLAPSED:
        m_layoutMargins.clear();
        m_spacerSizes.clear();
    
        collapseLayout(this->layout());
        break;
      case CollapseExpandButton::State::EXPANDED:
        expandLayout(this->layout());
        break;
      }
    }
    
    void CollapsibleGroupBox::resizeCollapseButton()
    {
      const QScreen *scr = findScreenForWidget(this);
    
      if (scr == nullptr)
        return;
    
      const auto &size = this->size();
    
    #ifdef Q_OS_WIN
      qreal baseSize = 15.0;
      int yOffset = 5;
    #else
      qreal baseSize = 22.0;
      int yOffset = 0;
    #endif
    
      if (scr == nullptr)
        return;
    
      if (QString::compare(QApplication::style()->objectName(), "fusion") == 0)
        baseSize = 15.0;
    
      const qreal dpi = scr->logicalDotsPerInchX();
      const qreal btnSize = floor((baseSize * dpi / 96.0) + 0.5);
    
      m_clExpButton->setGeometry(size.width() - btnSize, yOffset, btnSize, btnSize);
    }
    
    void CollapsibleGroupBox::resizeEvent(QResizeEvent *)
    {
      resizeCollapseButton();
    }
    

    This seems to collapse and restore the boxes exactly as I would expect.