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 :
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)));
}