1
votes

This post is closely related to this question about animating a QVBoxLayout, which got me half-way back when I found it.

I have a QVBoxLayout containing a few widgets. The last of them is only visible in certain situations, so I want to animate it when it appears and disappears.
I'm currently using QPropertyAnimation to animate the maximumHeight property of the widget I want to show and hide. However, the issue here is that I don't know what size the widget should be: the QVBoxLayout decides that, based on the parent window size and stretch factors. Therefore, I don't know what values I should give to QPropertyAnimation::setStartValue and setEndValue, for the show animation (when hiding, it should work to simply use the current height).

It sort-of works to simply give a very excessive value (such as 2000 px, when you expect it to rarely be larger than 400), since we're animating the maximum height, but that has issues. Namely, the finished signal is emitted after the full animation duration, even if the animation stopped (visually) long ago, as the widget reached its allotted size.
When hiding, the issue is instead that the animation is delayed: from 2000 px to the current height(), nothing happens; after that, it collapses quickly.

I then considered animating the stretch factors instead, but couldn't find a way to, since they're not exposed as properties.
As a last resort, it might be possible to subclass the layout and create a property that adjusts the stretch factor of the last two items, but that feels like a huge hack in several ways.

How can I animate this in a nice way?

Just in case somebody's going to ask for a code example, I wrote a simple test application. If you weren't going to ask, you can probably skip looking through it.

#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>
#include <QTextEdit>
#include <QPropertyAnimation>

QTextEdit *topTextEdit = nullptr;
QTextEdit *bottomTextEdit = nullptr;

void toggleButtonClicked() {
    QPropertyAnimation *anim = new QPropertyAnimation(bottomTextEdit, 
                                                      "maximumHeight");
    anim->setDuration(1200);

    if (bottomTextEdit->isVisible()) {
        anim->setStartValue(1000);
        anim->setEndValue(0);
        QObject::connect(anim, &QPropertyAnimation::finished, [] {
            bottomTextEdit->hide();
            topTextEdit->append("Animation finished");
        });
    }
    else {
        bottomTextEdit->show();
        anim->setStartValue(0);
        anim->setEndValue(1000);
        QObject::connect(anim, &QPropertyAnimation::finished, [] {
            topTextEdit->append("Animation finished");
        });
    }

    anim->start(QAbstractAnimation::DeleteWhenStopped);
    topTextEdit->append("Animation started");
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    topTextEdit = new QTextEdit;
    bottomTextEdit = new QTextEdit;

    QPushButton *toggleButton = new QPushButton("Toggle");
    QObject::connect(toggleButton, &QPushButton::released,
                     &toggleButtonClicked);

    QVBoxLayout *vbox = new QVBoxLayout;
    vbox->addWidget(toggleButton);
    vbox->addWidget(topTextEdit, 10);
    vbox->addWidget(bottomTextEdit, 6);

    QWidget *widget = new QWidget;
    widget->setLayout(vbox);

    bottomTextEdit->setMaximumHeight(0);
    bottomTextEdit->hide();
    widget->show();

    return a.exec();
}
1

1 Answers

1
votes

I ended up going with the subclassing route, since I couldn't think of anything else, and this question didn't see a lot of activity.

The subclass (must be in a header file or you'll get undefined reference to vtable errors):

class EVBoxLayout : public QVBoxLayout {
    Q_OBJECT
    Q_PROPERTY(int lastStretch READ lastStretch WRITE setLastStretch)

public:
    EVBoxLayout() {}
    int lastStretch() const { return this->stretch(this->count() - 1); }
    void setLastStretch(int newStretch) {
        this->setStretch(this->count() - 1, newStretch);
    }
};

After implementing that, replace the QVBoxLayout with an EVBoxLayout, and create the animation using

QPropertyAnimation *anim = new QPropertyAnimation(vbox, "lastStretch");

There are several points to consider for this to work:

  • For the animation to be smooth, you should probably use very large stretch factors; since they are integers, an animation from e.g. 1 to 5 will be very choppy. I went with a 1000:600 ratio, so my animation is from 1 to 600 for the bottom widget, which I want to be the smaller one.
  • Make sure to set the minimum height of the widget to 1 (not 0!), or the animation may begin part-way.
  • Make sure to animate the stretch factor from 1, not from 0, or there will be a flicker when the animation begins.

The solution feels hacky, but it looks and works great in practice.
For completeness, here's the full test program, updated to use this fix:

main.h:

#ifndef MAIN_H
#define MAIN_H

#include <QVBoxLayout>

class EVBoxLayout : public QVBoxLayout {
    Q_OBJECT
    Q_PROPERTY(int lastStretch READ lastStretch WRITE setLastStretch)

public:
    int lastStretch() const { return this->stretch(this->count() - 1); }
    void setLastStretch(int newStretch) { this->setStretch(this->count() - 1, newStretch); }
};

#endif // MAIN_H

main.cpp:

#include "main.h"

#include <QApplication>
#include <QVBoxLayout>
#include <QPushButton>
#include <QTextEdit>
#include <QPropertyAnimation>

QTextEdit *topTextEdit = nullptr;
QTextEdit *bottomTextEdit = nullptr;   
EVBoxLayout *vbox = nullptr;

void toggleButtonClicked() {
    QPropertyAnimation *anim = new QPropertyAnimation(vbox, "lastStretch");
    anim->setDuration(250);

    // Without this, the scrollbar may appear (and then disappear again)
    // during animation.
    bottomTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    if (bottomTextEdit->isVisible()) {
        anim->setStartValue(600);
        anim->setEndValue(1);
        QObject::connect(anim, &QPropertyAnimation::finished, [] {
            bottomTextEdit->hide();
            topTextEdit->append("Animation finished");
            bottomTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        });
    }
    else {
        bottomTextEdit->show();
        anim->setStartValue(1);
        anim->setEndValue(600);
        QObject::connect(anim, &QPropertyAnimation::finished, [] {
            topTextEdit->append("Animation finished");
            bottomTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        });
    }

    anim->start(QAbstractAnimation::DeleteWhenStopped);
    topTextEdit->append("Animation started");
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    topTextEdit = new QTextEdit;
    bottomTextEdit = new QTextEdit;
    bottomTextEdit->setMinimumHeight(1);

    QPushButton *toggleButton = new QPushButton("Toggle");
    QObject::connect(toggleButton, &QPushButton::released,
                     &toggleButtonClicked);

    vbox = new EVBoxLayout;
    vbox->addWidget(toggleButton);
    vbox->addWidget(topTextEdit, 1000);
    vbox->addWidget(bottomTextEdit, 1);

    QWidget *widget = new QWidget;
    widget->setLayout(vbox);

    bottomTextEdit->hide();
    widget->show();

    return a.exec();
}

I'll wait another day or so to accept this as an answer, in case someone comes along with something less hacky (such as a way to calculate the exact final size from the stretch factors).