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

230 Views Asked by At

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

1

There are 1 best solutions below

0
On BEST ANSWER

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