Cesar
Cesar

Reputation: 379

Slide animated text on hover

.h

class myButton : public QPushButton
{
    Q_OBJECT

public:

    QPropertyAnimation* anim;

    struct WidgetPos { int x = 0; int y = 0; int w = 0; int h = 0;  };
    WidgetPos wp;

    void CreateAnimation(QByteArray propertyName)
    {
        if (propertyName == "geometry")
        {
            anim = new QPropertyAnimation(this, propertyName);
            this->anim->setDuration(100);
            this->anim->setEasingCurve(QEasingCurve::Linear);

            this->wp.x = this->x();
            this->wp.y = this->y();
            this->wp.w = this->width();
            this->wp.h = this->height();
        }
    }



    myButton(QWidget* parent = 0) : QPushButton(parent) {}



    bool eventFilter(QObject* obj, QEvent* event)
    {
 
        if (event->type() == QEvent::Enter)
        {
            if (!this->wp.x)
                this->CreateAnimation("geometry");

            this->anim->stop();

            this->anim->setStartValue(
                QRect(this->x(), this->y(), this->width(), this->height()));
            this->anim->setEndValue(
                QRect(this->x(), this->y(), (this->wp.w + 200) - this->width(), this->height()));

            this->anim->start();

        }
        else if (event->type() == QEvent::Leave)
        {
            this->anim->stop();

            this->anim->setStartValue(
                QRect(this->x(), this->y(), (this->wp.w + 200) - this->width(), this->height()));
            this->anim->setEndValue(
                QRect(this->wp.x, this->wp.x, this->wp.w, this->wp.h));

            this->anim->start();
        }

        return QWidget::eventFilter(obj, event);
       
    }

};

.cpp

QtWidgetsApplication::QtWidgetsApplication(QWidget * parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);

    QPushButton* btn = new myButton(this);

    btn->setGeometry(100, 100, 50, 40);
    btn->setStyleSheet(R"(QPushButton {
        background-image: url(:/tutorial.png);
        background-repeat: no-repeat; }
    )");

    QLabel* labl = new QLabel(btn);
    labl->setObjectName("label");
    labl->setGeometry(32, 0, btn->width() + 32, btn->height());
    labl->setText("Hello World");
    labl->setAlignment(Qt::AlignCenter);
    labl->show();

    btn->installEventFilter(btn);
    return;
}

So far what I did result on:

enter image description here

If I move the mouse on it so fast it becomes messy, and the "closing" animation <= isn't working.

I'm struggling with the calculation of the animation QRect and handling it when there's an animation already running.

The goal is to create a smooth animation effect similar to see in this gif:

enter image description here

Upvotes: 1

Views: 324

Answers (3)

linuxfever
linuxfever

Reputation: 3823

I think the reason for the issue you are having is because when you are leaving the widget you set the start animation to the maximum width the button could take instead of starting it from the current width. I've implemented my own QPushButton subclass in the following way which seems to achieve the result you need. Instead of creating an event filter, I'll just override the enter and leave event. We'll also need to update the initial geometry every time the widget is moved or resized (outside of the animation), so I'm overriding the move and resize event as well.

// MyButton.h

class MyButton : public QPushButton
{
public:
    MyButton(QWidget* parent = nullptr);

    ~MyButton() = default;

protected:

    void enterEvent(QEvent *event) override;
    void leaveEvent(QEvent* event) override;
    void moveEvent(QMoveEvent *event) override;
    void resizeEvent(QResizeEvent* event) override;

private:

    QPropertyAnimation* m_animation;
    QRect m_init_geometry;
    double m_duration;
    double m_extension;
};

Here is the implementation:

// MyButton.cpp

MyButton::MyButton(QWidget* parent)
    : QPushButton(parent)
    , m_animation(nullptr)
    , m_init_geometry()
    , m_duration(200)
    , m_extension(100)
{
    m_animation = new QPropertyAnimation(this, "geometry", this);
    m_animation->setDuration(m_duration);
    m_animation->setEasingCurve(QEasingCurve::Linear);
    m_init_geometry = geometry();
}

void MyButton::enterEvent(QEvent *event)
{
    QPushButton::enterEvent(event);
    
    m_animation->stop();
    // update the duration so that we get a uniform speed when triggering this animation midway
    m_animation->setDuration(((m_init_geometry.width() + m_extension - width())/m_extension)*m_duration);
    m_animation->setStartValue(geometry());
    m_animation->setEndValue(QRectF(m_init_geometry.x(), m_init_geometry.y(), m_init_geometry.width() + m_extension, m_init_geometry.height()));
    m_animation->start();
}

void MyButton::leaveEvent(QEvent *event)
{
    QPushButton::leaveEvent(event);
    
    m_animation->stop();
    // update the duration so that we get a uniform speed when triggering this animation midway
    m_animation->setDuration(((width() - m_init_geometry.width())/m_extension)*m_duration);
    m_animation->setStartValue(geometry());
    m_animation->setEndValue(m_init_geometry);
    m_animation->start();
}

void MyButton::moveEvent(QMoveEvent *event)
{
    // ignore the move event if it's due to the animation, otherwise store the new geometry
    if(m_animation->state() == QPropertyAnimation::Running) return;
    QPushButton::moveEvent(event);
    m_init_geometry.setTopLeft(event->pos());
}

void MyButton::resizeEvent(QResizeEvent *event)
{
    // ignore the move event if it's due to the animation, otherwise store the new geometry
    if(m_animation->state() == QPropertyAnimation::Running) return;
    QPushButton::resizeEvent(event);
    m_init_geometry.setSize(event->size());
}

Notice that the start value of the closing animation is the current geometry and not the initial geometry plus the extended width. I'm updating reducing the duration of the opening animation linearly depending on how close the current width is to the full extended width; similarly for the closing animation. The rest now is very similar to your code:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    auto* btn = new MyButton(this);
    btn->setGeometry(100, 100, 60, 80);

    btn->setStyleSheet(R"(QPushButton {
        background-image: url(:/ubuntu.png);
        background-repeat: no-repeat;
        background-origin: content;
        background-position: left center;}
    )");

    auto* labl = new QLabel("Hello World", btn);
    labl->setAlignment(Qt::AlignCenter);
    labl->setGeometry(btn->width(), 0, labl->width(), btn->height());
}

The result looks like this

result

Upvotes: 2

TeaAge Solutions
TeaAge Solutions

Reputation: 473

You can try this approach: Calculate the start and end rect only once and set in the animation object.

On enter you start the animation as before. On leave you can change the direction of the animation from play forward to backward.

You can run the animation backwards with

this->anim->setDirection( QAbstractAnimation::Backward );

I don't know if you must surround it with

this->anim->pause();
//[...]
this->anim->resume();

Maybe you must experience with it a little.

Also, maybe you must keep track of

1.) Did you start a animation in forward and backward already for not start it twice or even more often, e.g. use an enum to safe state in a member:

enum class eState { Stopped, Forward, Backward };

2.) Test if the animation is still running or finished already for eventually start a new animation in either forward or backward direction, e.g. test with

this->anim->currentTime() < this->anim->totalDuration();
// or just query the state
this->anim->state() == QAbstractAnimation::Stopped;

I hope I could help you to solve your problem.

EDIT My point 1.) you can also solve with the methods of the animation class by testing:

this->anim->direction(); // and...
this->anim->state();

Upvotes: 0

Parisa.H.R
Parisa.H.R

Reputation: 3883

I try this way:

in mybutton.h

#ifndef MYBUTTON_H
#define MYBUTTON_H

#include <QLabel>
#include <QPushButton>



class MyButton : public QPushButton
{
    Q_OBJECT
public:
    MyButton(QWidget* parent = nullptr);

    // QObject interface
public:
    bool eventFilter(QObject *watched, QEvent *event);

signals:
    void mouseEnter();
    void mouseLeave();



private:
};

#endif // MYBUTTON_H


in mybutton.cpp

#include "mybutton.h"

#include <QEvent>
#include <QLabel>

MyButton::MyButton(QWidget *parent):
    QPushButton(parent)
{


}

bool MyButton::eventFilter(QObject *watched, QEvent *event)
{
    if (event->type() == QEvent::HoverEnter)
    {
        emit mouseEnter();

    }
    else if (event->type() == QEvent::HoverLeave)
    {

        emit mouseLeave();

    }
}

I use signal and in MainWindow class UI I add widget and layouts:

enter image description here

in mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    QLabel *labl;
    QLabel *labl2;

};
#endif // MAINWINDOW_H


in mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <mybutton.h>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    MyButton* btn = new MyButton(this);
    btn->setObjectName("button 1 ");
    btn->setText("btn 1");

    btn->setGeometry(ui->widget->x(), 100, 50, 50);

    ui->widget->layout()->addWidget(btn);

    //    QLabel

    labl= new QLabel(btn,Qt::ToolTip);
    connect(btn,&MyButton::mouseEnter,this,[btn,this](){

        labl->setObjectName("label");
        labl->setGeometry(btn->x()+this->x()+btn->width()+10, btn->y()+this->y()+btn->height()+15,
                          labl->width(), labl->height());

        labl->setText("Hello World");
        labl->setAlignment(Qt::AlignCenter);
        labl->show();

    });

    connect(btn,&MyButton::mouseLeave,this,[this](){

        labl->hide();

    });


    btn->installEventFilter(btn);

    MyButton* btn2 = new MyButton(this);
    btn2->setObjectName("button 2 ");
    btn2->setText("btn 2");

    btn2->setGeometry(ui->widget->x(), 100, 50, 50);

    ui->widget->layout()->addWidget(btn2);

    //    QLabel

    labl2= new QLabel(btn2,Qt::ToolTip);
    connect(btn2,&MyButton::mouseEnter,this,[btn2,this](){

        labl2->setObjectName("label");
        labl2->setGeometry(btn2->x()+this->x()+btn2->width()+10, btn2->y()+this->y()+btn2->height()+15,
                          labl2->width(), labl2->height());

        labl2->setText("Hello World 2");
        labl2->setAlignment(Qt::AlignCenter);
        labl2->show();

    });

    connect(btn2,&MyButton::mouseLeave,this,[this](){

        labl2->hide();

    });


    btn->installEventFilter(btn);
    btn2->installEventFilter(btn2);



}

MainWindow::~MainWindow()
{
    delete ui;
}

and this is my result:

enter image description here

By using the same color in the stylesheet you can have what you show in your Gif.

Upvotes: 0

Related Questions