Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
570 views
in Technique[技术] by (71.8m points)

c++ - How to make an expandable/collapsable section widget in Qt

I would like to create a custom widget in Qt with the following features:

  • It is a container
  • It may be populated with any Qt layout
  • It may be inside any Qt layout
  • A button allows to collapse/fold vertically the content, so only the button is visible, all the contained layout is invisible.
  • The previous button allows to expand/unfold it again to the size of the layout content.
  • The expanding/collapsing is based on sizes (not on show/hide) to allows animation.
  • Usable in QDesigner

To provide an idea, here is an image of a similar widget (not Qt): enter image description here

I already have a frame that work correctly and is exposed in QDesigner. I need now to make it to extend/collapse, which does not seem so simple.

I tried to play with resize(), sizePolicy(), sizeHint() but that does not work: When the frame is collapsed I got following values:

sizeHint: (500,20)
size    : (500,20)
closestAcceptableSize: (518,150)
Painted size: (518, 150)

QLayout::closestAcceptableSize is not part of the widget so I cannot change it.

Any hint or/and code snippet to achieve that?

EDITED: Here a simple example. I removed all except necessary.

main.cpp example

#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

#include "section.hpp"


using namespace myWidgets;
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);


    // Create the main Window
    QWidget window;
    window.resize(500,500);
    window.setStyleSheet("QPushButton:{background-color:rgba(128,128,128,192);}");

    // Create the main window layout
    QVBoxLayout topLayout(&window);
    QWidget *w1 = new QWidget();
    w1->setStyleSheet("background-color:rgba(128,128,128,192);");
    topLayout.addWidget(w1);

    Section section(&window);
    topLayout.addWidget(&section);

    QVBoxLayout inLayout(&section);
    QPushButton *button = new QPushButton();
    button->setMinimumHeight(100);
    inLayout.addWidget(button);

    QWidget *w2 = new QWidget();
    w2->setStyleSheet("background-color:rgba(128,128,128,192);");
    topLayout.addWidget(w2);



    window.show();

    return a.exec();
}

Section.hpp

#ifndef SECTION_HPP
#define SECTION_HPP

#include <QPushButton> //for the expand/collapse button
#include <QtDesigner/QDesignerExportWidget>
#include <QLayout>
#include <QPainter>
#include <QPaintEvent>
#include <QDebug>


// Compatibility for noexcept, not supported in vsc++
#ifdef _MSC_VER
#define noexcept throw()
#endif

#if defined SECTION_BUILD
    #define SECTION_BUILD_DLL_SPEC Q_DECL_EXPORT
#elif defined SECTION_EXEC
    #define SECTION_BUILD_DLL_SPEC
#else
    #define SECTION_BUILD_DLL_SPEC Q_DECL_IMPORT
#endif

namespace myWidgets
{

class SECTION_BUILD_DLL_SPEC Section : public QWidget
{
    Q_OBJECT

    Q_PROPERTY( bool is_expanded MEMBER isExpanded)

public:
    // Constructor, standard
    explicit Section( QWidget *parent=0 ): QWidget(parent),
        expandButton(this)
    {
        expandButton.resize(20,20);
        expandButton.move(0,0);
        expandButton.connect(&expandButton, &QPushButton::clicked,
                             this, &Section::expandCollapseEvent);

        QMargins m= contentsMargins();
        m.setTop(m.top()+25);
        setContentsMargins(m);
        //setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Minimum);

    }

    virtual void expand( bool expanding ) noexcept
    {
        resize(sizeHint());
        isExpanded = expanding;
        updateGeometry();

qDebug() << (isExpanded? "expanded":"collapsed") << sizeHint() << QWidget::size() <<
            parentWidget()->layout()->closestAcceptableSize(this, size());
    }

    virtual QSize sizeHint() const noexcept override
    {
        if (isExpanded) return QSize(layout()->contentsRect().width(),
                                     layout()->contentsRect().height());
        else return QSize(layout()->contentsRect().width(), 20);
    }

    // Implement custom appearance
    virtual void paintEvent(QPaintEvent *e) noexcept override
    {
        (void) e; //TODO: remove
        QPainter p(this);
        p.setClipRect(e->rect());
        p.setRenderHint(QPainter::Antialiasing );
        p.fillRect(e->rect(), QColor(0,0,255,128));
    }

protected:

    // on click of the expandButton, collapse/expand this widget
    virtual void expandCollapseEvent() noexcept
    {
        expand(!isExpanded);
    }


    bool isExpanded = true; //whenever the section is collapsed(false) or expanded(true)
    QPushButton expandButton; //the expanding/collapsing button
};

}


#endif // SECTION_HPP
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I stumbled upon the same problem and solved it by implementing the collapsible widget as a QScrollArea whose maximum height is animated by a QPropertyAnimation.

But since I don't use QDesigner, I can't tell you if it works there.

I still have one problem: Instead of only expanding towards the bottom direction, the collapsible widget can expand towards the top and bottom. This can cause widgets located above it to shrink if they haven't reached their minimum height, yet. But this is really a detail compared to the fact that we have to build this thing ourselves…

Spoiler.h

#include <QFrame>
#include <QGridLayout>
#include <QParallelAnimationGroup>
#include <QScrollArea>
#include <QToolButton>
#include <QWidget>

class Spoiler : public QWidget {
    Q_OBJECT
private:
    QGridLayout mainLayout;
    QToolButton toggleButton;
    QFrame headerLine;
    QParallelAnimationGroup toggleAnimation;
    QScrollArea contentArea;
    int animationDuration{300};
public:
    explicit Spoiler(const QString & title = "", const int animationDuration = 300, QWidget *parent = 0);
    void setContentLayout(QLayout & contentLayout);
};

Spoiler.cpp

#include <QPropertyAnimation>

#include "Spoiler.h"

Spoiler::Spoiler(const QString & title, const int animationDuration, QWidget *parent) : QWidget(parent), animationDuration(animationDuration) {
    toggleButton.setStyleSheet("QToolButton { border: none; }");
    toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    toggleButton.setArrowType(Qt::ArrowType::RightArrow);
    toggleButton.setText(title);
    toggleButton.setCheckable(true);
    toggleButton.setChecked(false);

    headerLine.setFrameShape(QFrame::HLine);
    headerLine.setFrameShadow(QFrame::Sunken);
    headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);

    contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }");
    contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    // start out collapsed
    contentArea.setMaximumHeight(0);
    contentArea.setMinimumHeight(0);
    // let the entire widget grow and shrink with its content
    toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight"));
    toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight"));
    toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight"));
    // don't waste space
    mainLayout.setVerticalSpacing(0);
    mainLayout.setContentsMargins(0, 0, 0, 0);
    int row = 0;
    mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft);
    mainLayout.addWidget(&headerLine, row++, 2, 1, 1);
    mainLayout.addWidget(&contentArea, row, 0, 1, 3);
    setLayout(&mainLayout);
    QObject::connect(&toggleButton, &QToolButton::clicked, [this](const bool checked) {
        toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow);
        toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward);
        toggleAnimation.start();
    });
}

void Spoiler::setContentLayout(QLayout & contentLayout) {
    delete contentArea.layout();
    contentArea.setLayout(&contentLayout);
    const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight();
    auto contentHeight = contentLayout.sizeHint().height();
    for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) {
        QPropertyAnimation * spoilerAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(i));
        spoilerAnimation->setDuration(animationDuration);
        spoilerAnimation->setStartValue(collapsedHeight);
        spoilerAnimation->setEndValue(collapsedHeight + contentHeight);
    }
    QPropertyAnimation * contentAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1));
    contentAnimation->setDuration(animationDuration);
    contentAnimation->setStartValue(0);
    contentAnimation->setEndValue(contentHeight);
}

How to use it:

…
auto * anyLayout = new QVBoxLayout();
anyLayout->addWidget(…);
…
Spoiler spoiler;
spoiler.setContentLayout(*anyLayout);
…

Spoiler example


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...