Search code examples
pythonqtopacityqwidgetpyside6

Fade widgets out using animation to transition to a screen


I want to implement a button (already has a custom class) that when clicked, fades out all the widgets on the existing screen before switching to another layout (implemented using QStackedLayout)

I've looked at different documentations and guides on PySide6 on how to animate fading in/out but nothing seems to be working. Not sure what is wrong with the code per se bit I've done the debugging and the animation class is acting on the widget

I assume that to make the Widget fade, I had to create QGraphicsOpacityEffect with the top-level widget being the parent, then adding the QPropertyAnimation for it to work.

main.py

# Required Libraries for PySide6/Qt
from PySide6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QMainWindow, QSystemTrayIcon, QStackedLayout, QGraphicsOpacityEffect
from PySide6.QtGui import QIcon, QPixmap, QFont, QLinearGradient, QPainter, QColor
from PySide6.QtCore import Qt, QPointF, QSize, QVariantAnimation, QAbstractAnimation, QEasingCurve, QPropertyAnimation, QTimer

# For changing the taskbar icon
import ctypes
import platform

# For relative imports
import sys
sys.path.append('../classes')

# Classes and Different Windows
from classes.getStartedButton import getStartedButton

class window(QMainWindow):

    # Set up core components of window
    def __init__(self,h,w):
        # Gets primary parameters of the screen that the window will display in
        self.height = h
        self.width = w
        
        super().__init__()

        # Required to change taskbar icon
        if platform.system() == "Windows":
            ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
                "com.random.appID")

        # Multiple screens will overlay each other
        self.layout = QStackedLayout()
        
        # Init splash screen - this is first
        self.splashScreen()
        
        # Function to init other screens
        self.initScreens()      
        
        # Must create main widget to hold the stacked layout, or another window will appear
        main_widget_holder = QWidget()
        main_widget_holder.setLayout(self.layout)
        self.setCentralWidget(main_widget_holder)
        

    def initScreens(self):
        
        apiScreen = QWidget()
        
        another_button = QPushButton("Test")
        another_layout = QVBoxLayout()
        another_layout.addWidget(another_button)
        apiScreen.setLayout(another_layout)
        self.layout.addWidget(apiScreen)

    # Window definition for splash screen
    def splashScreen(self):
        """Window that displays the splash screen
        """

        # Widget that holds all the widgets in the splash screen
        self.placeholder = QWidget()

        # Logo & Title Component
        logo = QLabel("")
        logo.setPixmap(QPixmap("image.png"))

        # Align logo on right side of the split
        logo.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        logo.setStyleSheet(
            "border-right:3px solid white;background-color: rgb(22,22,22);")

        title = QLabel("Another\nApp")
        title.setStyleSheet("padding-left:2px; font-size:36px")

        # Header to hold title and logo
        header_layout = QHBoxLayout()
        header_layout.addWidget(logo)
        header_layout.addWidget(title)

        header = QWidget()
        header.setStyleSheet("margin-bottom:20px")

        # Assign header_layout to the header widget
        header.setLayout(header_layout)

        # Button Component
        button = getStartedButton("Get Started  ")

        # Set max width of the button to cover both text and logo
        button.setMaximumWidth(self.placeholder.width())
        
        button.clicked.connect(self.transition_splash)

        # Vertical Layout from child widget components
        title_scrn = QVBoxLayout()
        title_scrn.addWidget(header)
        title_scrn.addWidget(button)

        # Define alignment to be vertically and horizontal aligned (Prevents button from appearing on the bottom of the app as well)
        title_scrn.setAlignment(Qt.AlignCenter)

        # Enlarge the default window size of 640*480 to something bigger (add 50px on all sides)
        self.placeholder.setLayout(title_scrn)
        self.placeholder.setObjectName("self.placeholder")

        self.placeholder.setMinimumSize(
            self.placeholder.width()+100, self.placeholder.height()+100)
        
        self.setCentralWidget(self.placeholder)

        # Grey/Black Background
        self.setStyleSheet(
            "QMainWindow{background-color: rgb(22,22,22);}QLabel{color:white} #button{padding:25px; border-radius:15px;background: qlineargradient(x1:0, y1:0,x2:1,y2:1,stop: 0 #00dbde, stop:1 #D600ff); border:1px solid white}")
        self.setMinimumSize(self.width/3*2,self.height/3*2)
       
        self.layout.addWidget(self.placeholder)
    def transition_splash(self):
        opacityEffect = QGraphicsOpacityEffect(self.placeholder)
        self.placeholder.setGraphicsEffect(opacityEffect)
        animationEffect = QPropertyAnimation(opacityEffect, b"opacity")
        animationEffect.setStartValue(1)
        animationEffect.setEndValue(0)
        animationEffect.setDuration(2500)
        animationEffect.start()
        timer = QTimer()
        timer.singleShot(2500,self.change_layout)
    
    def change_layout(self):
        self.layout.setCurrentIndex(1)

# Initialise program
if __name__ == "__main__":
    app = QApplication([])
    page = window(app.primaryScreen().size().height(),app.primaryScreen().size().width())
    page.show()
    sys.exit(app.exec())

button.py

# Required Libraries for PySide6/Qt
from PySide6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QMainWindow, QSystemTrayIcon, QStackedLayout, QGraphicsOpacityEffect
from PySide6.QtGui import QIcon, QPixmap, QFont, QLinearGradient, QPainter, QColor
from PySide6.QtCore import Qt, QPointF, QSize, QVariantAnimation, QAbstractAnimation, QEasingCurve

# Custom PushButton for splash screen
class getStartedButton(QPushButton):

    getStartedButtonColorStart = QColor(0, 219, 222)
    getStartedButtonColorInt = QColor(101, 118, 255)
    getStartedButtonColorEnd = QColor(214, 0, 255)

    def __init__(self, text):
        super().__init__()
        self.setText(text)

        # Setting ID so that it can be used in CSS
        self.setObjectName("button")
        self.setStyleSheet("font-size:24px")

        # Button Animation
        self.getStartedButtonAnimation = QVariantAnimation(
            self, startValue=0.42, endValue=0.98, duration=300)
        self.getStartedButtonAnimation.valueChanged.connect(
            self.animate_button)
        self.getStartedButtonAnimation.setEasingCurve(QEasingCurve.InOutCubic)
        

    def enterEvent(self, event):
        self.getStartedButtonAnimation.setDirection(QAbstractAnimation.Forward)
        self.getStartedButtonAnimation.start()
        # Suppression of event type error
        try:
            super().enterEvent(event)
        except TypeError:
            pass

    def leaveEvent(self, event):
        self.getStartedButtonAnimation.setDirection(
            QAbstractAnimation.Backward)
        self.getStartedButtonAnimation.start()
        # Suppression of event type error
        try:
            super().enterEvent(event)
        except TypeError:
            pass

    def animate_button(self, value):
        grad = "background-color: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 {startColor}, stop:{value} {intermediateColor}, stop: 1.0 {stopColor});font-size:24px".format(
            startColor=self.getStartedButtonColorStart.name(), intermediateColor=self.getStartedButtonColorInt.name(), stopColor=self.getStartedButtonColorEnd.name(), value=value
        )
        self.setStyleSheet(grad)

For context, I've looked at other questions already on SO and other sites such as


Solution

  • Not sure what is wrong with the code

    The QPropertyAnimation object is destroyed before it gets a chance to start your animation. Your question has already been solved here.

    To make it work, you must persist the object:

    def transition_splash(self):
        opacityEffect = QGraphicsOpacityEffect(self.placeholder)
        self.placeholder.setGraphicsEffect(opacityEffect)
        self.animationEffect = QPropertyAnimation(opacityEffect, b"opacity")
        self.animationEffect.setStartValue(1)
        self.animationEffect.setEndValue(0)
        self.animationEffect.setDuration(2500)
        self.animationEffect.start()
    
        # Use the finished signal instead of the QTimer
        self.animationEffect.finished.connect(self.change_layout)