Search code examples
python-3.xanimationpyqt5qstackedwidget

PyQt5: QStackedWidget background is visible/white during animation


I'm using Python 3.8 and PyQt5 to write a GUI with "Designer" installed with the Qt5-Package and then translated (with pyuic5) to a .py file.

Actually I have a fading animation, when the current index of the stacked widget changes (it changes/toggles when clicking the push button).

My Problem... A white background during the animation to the other page of the stacked widget. I want to have it transparent eg. not visible, so that the image in the background is visible (it is located in another file 'resources').

I already tried to set a fixed background to the stacked widget. With this, no white color is visible during animation. However, my goal is to set it transparent to see the background. On some pages I found the info to set the stylesheet to "background:transparent" or to set attribute to "WA_TranslucentBackground", but all of that didn't work for me. The stylesheet background-color: rgba(0,0,0,0) worked neither.

I hope somebody can help me with my problem. If any information is missing please comment. Thank in advance.


UPDATE: One code snippet to test...

There's a very small code snippet that works (I don't remeber from which website I have it). I colored the background green, so the problem is visible. Running the following code with Python 3.8 I got...

IncorrectFadingAnimationAtQStackedWidgetSimplified

It's clearly visible that there I have the same problem with the white background during animation.

import sys
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class FaderWidget(QWidget):
    def __init__(self, old_widget, new_widget): 
        QWidget.__init__(self, new_widget)    
        self.old_pixmap = QPixmap(new_widget.size())
        old_widget.render(self.old_pixmap)
        self.pixmap_opacity = 1.0  
        self.timeline = QTimeLine()
        self.timeline.valueChanged.connect(self.animate)
        self.timeline.finished.connect(self.close)
        self.timeline.setDuration(333)
        self.timeline.start()  
        self.resize(new_widget.size())
        self.show()
    
    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        painter.setOpacity(self.pixmap_opacity)
        painter.drawPixmap(0, 0, self.old_pixmap)
        painter.end()
    
    def animate(self, value):
        self.pixmap_opacity = 1.0 - value
        self.repaint()

class StackedWidget(QStackedWidget):
    def __init__(self, parent = None):
        QStackedWidget.__init__(self, parent)
    
    def setCurrentIndex(self, index):
        self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
        QStackedWidget.setCurrentIndex(self, index)
    
    def setPage1(self):
        self.setCurrentIndex(0)
    
    def setPage2(self):
        self.setCurrentIndex(1)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = QWidget()
    stack = StackedWidget()
    window.setStyleSheet("background-color: #0f0;")  # To see problem
    stack.addWidget(QCalendarWidget())
    editor = QTextEdit()
    editor.setPlainText("Hello world! "*100)
    stack.addWidget(editor)
    
    page1Button = QPushButton("Page 1")
    page2Button = QPushButton("Page 2")
    page1Button.clicked.connect(stack.setPage1)
    page2Button.clicked.connect(stack.setPage2)
    
    layout = QGridLayout(window)
    layout.addWidget(stack, 0, 0, 1, 2)
    layout.addWidget(page1Button, 1, 0)
    layout.addWidget(page2Button, 1, 1)
    
    window.show()
    
    sys.exit(app.exec_())

Hope now it's easier to help.


I tried to simplify my python code but it's still a bit of code...

main.py:

from sys import exit as sys_exit
from sys import argv as sys_argv
from outputFile import Ui_MainWindow
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QApplication, QStackedWidget, QWidget

import types
from funcs import FaderWidget, setCurrentIndex


class MyMainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.setupUi(self)
        self.pushButton.clicked.connect(self.btnFunc)

        ''' Overwrite method 'setCurrentIndex' from StackedWidget to animate it '''
        self.stackedWidget.setCurrentIndex = types.MethodType(setCurrentIndex, self.stackedWidget)

    def btnFunc(self):
        index = self.stackedWidget.currentIndex()
        if index == 1:
            self.stackedWidget.setCurrentIndex(0)
        else:
            self.stackedWidget.setCurrentIndex(1)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys_argv)
    MainWindow = MyMainWindow()
    MainWindow.show()
    sys_exit(app.exec_())

funcs.py:

from PyQt5.QtWidgets import QStackedWidget, QWidget
from PyQt5.QtCore import QTimeLine
from PyQt5.QtGui import QPixmap, QPainter

class FaderWidget(QWidget):
    def __init__(self, old_widget, new_widget):
        QWidget.__init__(self, new_widget)

        self.old_pixmap = QPixmap(new_widget.size())
        old_widget.render(self.old_pixmap)
        self.pixmap_opacity = 1.0

        self.timeline = QTimeLine()
        self.timeline.valueChanged.connect(self.animate)
        self.timeline.finished.connect(self.close)
        self.timeline.setDuration(333)
        self.timeline.start()

        self.resize(new_widget.size())
        self.show()

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        painter.setOpacity(self.pixmap_opacity)
        painter.drawPixmap(0, 0, self.old_pixmap)
        painter.end()

    def animate(self, value):
        self.pixmap_opacity = 1.0 - value
        self.repaint()


def setCurrentIndex(self, index):
    self.fader_widget = FaderWidget(self.currentWidget(), self.widget(index))
    QStackedWidget.setCurrentIndex(self, index)

Preview of my problem: IncorrectFadingAnimationAtQStackedWidget


Solution

  • The problem is that rendering a widget doesn't take into account the background of the parent, but only the widget's own background:

    If you enable this option, the widget's background is rendered into the target even if autoFillBackground is not set.

    This means that render will use the widget's palette backgroundRole as background color, no matter if it's "transparent".

    Disabling the default DrawWindowBackground flag won't help you much, even if you fill the pixmap with Qt.transparent, since it will overlap on the existing widget, making it "blurred".

    The solution is to render the top level window, but only using the widget's geometry:

    class FaderWidget(QWidget):
        def __init__(self, old_widget, new_widget):
            QWidget.__init__(self, new_widget)
    
            self.old_pixmap = QPixmap(old_widget.size())
            window = old_widget.window()
            geo = old_widget.rect().translated(old_widget.mapTo(window, QtCore.QPoint()))
            window.render(self.old_pixmap, sourceRegion=QtGui.QRegion(geo))
            # ...
    

    Note that you should use the old widget size for both the QPixmap and the resize, and that's because if you resize the window the "new" widget will still have the previous size (the stacked widget size or the size it had until it was visible): geometries of QStackedWidget children are updated on resize only when they are visible or are going to be shown.