Search code examples
c++qtqt6qpropertyanimationqtoolbutton

Qt - Create Button with images which fade out & fade in in parallel on hoverEvent


In C++/Qt6, I would like to develop a button with an animation (I imagine that I'll have to use QParallelAnimationGroup) which fade out an image in parallel of a fade in another image as the icon of the button.

Here is what I want (made with a movie maker):

button fade out-fade in images, demo of the effect I'm looking for

I already developed a QToolButton with animations that makes the button fade out, then fade in (in sequence) with a new icon. I used QPropertyAnimation and QSequentialAnimationGroup Here is the code :

.h

#ifndef FADINGIMAGESPUSHBUTTON_H
#define FADINGIMAGESPUSHBUTTON_H

#include <QToolButton>
#include <QPropertyAnimation>
#include <QGraphicsOpacityEffect>
#include <QSequentialAnimationGroup>

class FadingImagesPushButton : public QToolButton
{
    Q_OBJECT
public:
    FadingImagesPushButton(QWidget *parent = nullptr, const QString & title = "", const QString & imageNormalPath = "", const QString & imageHoverPath = "");
protected:
    static const int s_AnimDuration;
    virtual bool event(QEvent * e) override;
    void hoverEnter();
    void hoverLeave();
    QString m_imageNormalPath;
    QString m_imageHoverPath;
    int m_curTimeAnim_toHover;
    int m_curTimeAnim_toNormal;

    QPropertyAnimation *m_AnimNormalFadeOut;
    QPropertyAnimation *m_AnimHoverFadeIn;
    QPropertyAnimation *m_AnimHoverFadeOut;
    QPropertyAnimation *m_AnimNormalFadeIn;
    QSequentialAnimationGroup *m_toHoverAnimGroup;
    QSequentialAnimationGroup *m_toNormalAnimGroup;

    QGraphicsOpacityEffect *m_effect;
protected slots:
    void switchToHoverImage();
    void switchToNormalImage();
    void deleteAndNullToHoverAnimGroup();
    void deleteAndNullToNormalAnimGroup();
};

#endif // FADINGIMAGESPUSHBUTTON_H

.cpp

#include "fadingimagespushbutton.h"
#include <QEvent>

const int FadingImagesPushButton::s_AnimDuration = 800;

FadingImagesPushButton::FadingImagesPushButton(QWidget *parent,const QString & title, const QString & imageNormalPath, const QString & imageHoverPath)
    :QToolButton(parent),m_imageNormalPath(imageNormalPath),m_imageHoverPath(imageHoverPath)
{
    setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
    setFixedSize(275,280);
    setIconSize(QSize(270,240));
    QFont font;
    font.setPointSize(18);
    setAttribute(Qt::WA_Hover);
    setFont(font);
    setText(title);
    setIcon(QIcon(m_imageNormalPath));

    m_effect = new QGraphicsOpacityEffect(this);
    setGraphicsEffect(m_effect);

    m_toHoverAnimGroup = nullptr;
    m_toNormalAnimGroup = nullptr;
}

bool FadingImagesPushButton::event(QEvent *e)
{
    switch(e->type())
        {
        case QEvent::HoverEnter:
            hoverEnter();
            return QToolButton::event(e);
        case QEvent::HoverLeave:
            hoverLeave();
            return QToolButton::event(e);
        default:
            break;
        }
    return QToolButton::event(e);
}

void FadingImagesPushButton::hoverEnter()
{
    int realDurationFadeOut = s_AnimDuration;
    int realDurationFadeIn = s_AnimDuration;
    if(m_toNormalAnimGroup)
    {
        int animGroupDuration = m_toNormalAnimGroup->currentTime();
        if(animGroupDuration < s_AnimDuration)
        {
            realDurationFadeOut = 0;
            realDurationFadeIn = animGroupDuration;
        }
        else
        {
            realDurationFadeOut = animGroupDuration - s_AnimDuration;
            realDurationFadeIn = s_AnimDuration;
        }
        m_toNormalAnimGroup->stop();
    }


    if(realDurationFadeOut !=0)
    {
        m_AnimNormalFadeOut = new QPropertyAnimation(m_effect,"opacity");
        m_AnimNormalFadeOut->setDuration(realDurationFadeOut);
        m_AnimNormalFadeOut->setStartValue(1.*((float)realDurationFadeOut/(float)s_AnimDuration));
        m_AnimNormalFadeOut->setEndValue(0.);
        m_AnimNormalFadeOut->setEasingCurve(QEasingCurve::InBack);
    }
    m_AnimHoverFadeIn = new QPropertyAnimation(m_effect,"opacity");
    m_AnimHoverFadeIn->setDuration(realDurationFadeIn);
    m_AnimHoverFadeIn->setStartValue(1. - ((float)realDurationFadeIn/(float)s_AnimDuration));
    m_AnimHoverFadeIn->setEndValue(1.);
    m_AnimHoverFadeIn->setEasingCurve(QEasingCurve::InBack);

    m_toHoverAnimGroup = new QSequentialAnimationGroup(this);
    if(realDurationFadeOut !=0)
        m_toHoverAnimGroup->addAnimation(m_AnimNormalFadeOut);
    m_toHoverAnimGroup->addAnimation(m_AnimHoverFadeIn);

    connect(m_toHoverAnimGroup,&QSequentialAnimationGroup::currentAnimationChanged, this, &FadingImagesPushButton::switchToHoverImage);
    connect(m_toHoverAnimGroup,&QSequentialAnimationGroup::finished, this, &FadingImagesPushButton::deleteAndNullToHoverAnimGroup);

    m_toHoverAnimGroup->start();
}

void FadingImagesPushButton::hoverLeave()
{
    int realDurationFadeOut = s_AnimDuration;
    int realDurationFadeIn = s_AnimDuration;
    if(m_toHoverAnimGroup)
    {
        int animGroupDuration = m_toHoverAnimGroup->currentTime();
        if(animGroupDuration < s_AnimDuration)
        {
            realDurationFadeOut = 0;
            realDurationFadeIn = animGroupDuration;
        }
        else
        {
            realDurationFadeOut = animGroupDuration - s_AnimDuration;
            realDurationFadeIn = s_AnimDuration;
        }
        m_toHoverAnimGroup->stop();
    }

    if(realDurationFadeOut !=0)
    {
        m_AnimHoverFadeOut = new QPropertyAnimation(m_effect,"opacity");
        m_AnimHoverFadeOut->setDuration(realDurationFadeOut);
        m_AnimHoverFadeOut->setStartValue(1.*((float)realDurationFadeOut/(float)s_AnimDuration));
        m_AnimHoverFadeOut->setEndValue(0.);
        m_AnimHoverFadeOut->setEasingCurve(QEasingCurve::InBack);
    }

    m_AnimNormalFadeIn = new QPropertyAnimation(m_effect,"opacity");
    m_AnimNormalFadeIn->setDuration(realDurationFadeIn);
    m_AnimNormalFadeIn->setStartValue(1.-((float)realDurationFadeIn/(float)s_AnimDuration));
    m_AnimNormalFadeIn->setEndValue(1.);
    m_AnimNormalFadeIn->setEasingCurve(QEasingCurve::InBack);

    m_toNormalAnimGroup = new QSequentialAnimationGroup(this);
    if(realDurationFadeOut !=0)
        m_toNormalAnimGroup->addAnimation(m_AnimHoverFadeOut);
    m_toNormalAnimGroup->addAnimation(m_AnimNormalFadeIn);

    connect(m_toNormalAnimGroup,&QSequentialAnimationGroup::currentAnimationChanged, this, &FadingImagesPushButton::switchToNormalImage);
    connect(m_toNormalAnimGroup,&QSequentialAnimationGroup::finished, this, &FadingImagesPushButton::deleteAndNullToNormalAnimGroup);

    m_toNormalAnimGroup->start();
}

void FadingImagesPushButton::switchToHoverImage()
{
    setIcon(QIcon(m_imageHoverPath));
}

void FadingImagesPushButton::switchToNormalImage()
{
    setIcon(QIcon(m_imageNormalPath));
}

void FadingImagesPushButton::deleteAndNullToHoverAnimGroup()
{
    m_toHoverAnimGroup = nullptr;
}

void FadingImagesPushButton::deleteAndNullToNormalAnimGroup()
{
    m_toNormalAnimGroup = nullptr;
}

and the result in demo :

button fade out-fade in images, already done in sequance


Solution

  • I manage to apply the effect expected, here is the code.

    NB : in my case the second image (hover) is a layer that I apply above the normal image, but if you want to merge two different images, just change applyComposition() a little

    NB2 : we will want to test the computing load. Applying two QPainter each 50 ms may not be the best way. Maybe prebuild 16 images with QPainter would be better

    .h

    #ifndef FADINGIMAGESPUSHBUTTON_H
    #define FADINGIMAGESPUSHBUTTON_H
    
    #include <QToolButton>
    #include <QElapsedTimer>
    
    class FadingImagesPushButton : public QToolButton
    {
        Q_OBJECT
    public:
        FadingImagesPushButton(QWidget *parent = nullptr, const QString & title = "", const QString & imageNormalPath = "", const QString & imageHoverPath = "");
    protected:
        static const int s_AnimDuration;
        virtual bool event(QEvent * e) override;
        QString m_imageNormalPath;
        QString m_imageHoverPath;
        QElapsedTimer toHoverTimer;
        QElapsedTimer toNormalTimer;
        void applyComposition();
        qreal m_currentOpacity;
        qreal m_leaveLastOpacity;
        qreal m_enterLastOpacity;
        QImage m_hoverLayer;
        QImage m_normalImage;
        bool m_leavingNow;
        bool m_enteringNow;
    protected slots:
        void hoverEnter();
        void hoverLeave();
    };
    
    #endif // FADINGIMAGESPUSHBUTTON_H
    

    .cpp

    #include "fadingimagespushbutton.h"
    #include <QEvent>
    #include <QPainter>
    #include <QImage>
    #include <QTimer>
    
    const int FadingImagesPushButton::s_AnimDuration = 800;
    
    FadingImagesPushButton::FadingImagesPushButton(QWidget *parent,const QString & title, const QString & imageNormalPath, const QString & imageHoverPath)
        :QToolButton(parent),m_imageNormalPath(imageNormalPath),m_imageHoverPath(imageHoverPath)
    {
        setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
        setText(title);
        setAttribute(Qt::WA_Hover);
        setIcon(QIcon(m_imageNormalPath));
        m_hoverLayer.load(m_imageHoverPath,"PNG");
        m_normalImage.load(m_imageNormalPath,"PNG");
    
        m_leaveLastOpacity = 1.;
        m_enterLastOpacity = 0.;
        m_leavingNow = false;
        m_enteringNow = false;
    }
    
    bool FadingImagesPushButton::event(QEvent *e)
    {
        switch(e->type())
            {
            case QEvent::HoverEnter:
                m_enteringNow = true;
                m_leavingNow = false;
                toHoverTimer.start();
                hoverEnter();
                return QToolButton::event(e);
            case QEvent::HoverLeave:
                m_leavingNow = true;
                m_enteringNow = false;
                toNormalTimer.start();
                hoverLeave();
                return QToolButton::event(e);
            default:
                break;
            }
        return QToolButton::event(e);
    }
    
    void FadingImagesPushButton::hoverEnter()
    {
        if(m_enteringNow && !m_leavingNow && toHoverTimer.elapsed() < toNormalTimer.elapsed())
        {
            m_currentOpacity =  m_leaveLastOpacity - (float)toHoverTimer.elapsed() / (float)s_AnimDuration;
            if(m_currentOpacity < 0.)
                m_currentOpacity = 0.;
            applyComposition();
            m_enterLastOpacity = m_currentOpacity;
            if(m_currentOpacity > 0.)
            {
                QTimer::singleShot(50,this,&FadingImagesPushButton::hoverEnter);
            }
            else
            {
                m_enteringNow = false;
            }
        }
    }
    
    void FadingImagesPushButton::hoverLeave()
    {
        if(m_leavingNow && !m_enteringNow && toNormalTimer.elapsed() < toHoverTimer.elapsed())
        {
            m_currentOpacity = m_enterLastOpacity + (float)toNormalTimer.elapsed() / (float)s_AnimDuration;
            if(m_currentOpacity > 1.)
                m_currentOpacity = 1.;
            applyComposition();
            m_leaveLastOpacity = m_currentOpacity;
            if(m_currentOpacity < 1.)
            {
                QTimer::singleShot(50,this,&FadingImagesPushButton::hoverLeave);
            }
            else
            {
                m_leavingNow = false;
            }
        }
    }
    
    void FadingImagesPushButton::applyComposition()
    {
        QImage result = m_hoverLayer.convertToFormat(QImage::Format_ARGB32);
        QPixmap mask(result.size());
        QPainter painterOpacity(&result);
        painterOpacity.setCompositionMode(QPainter::CompositionMode_DestinationOut);
        painterOpacity.setOpacity(m_currentOpacity);
        painterOpacity.drawPixmap(0,0,mask);
        painterOpacity.end();
    
        QPainter composition(&result);
        composition.setCompositionMode(QPainter::CompositionMode_DestinationOver);
        composition.drawImage(0,0,m_normalImage);
        composition.end();
    
        setIcon(QIcon(QPixmap::fromImage(result)));
    }
    

    Fade out - fade in images on button acheived