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
How to change the opacity of a PyQt5 window
https://www.pythonguis.com/tutorials/pyside6-animated-widgets/
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)
When I'm trying to make my app, I stumbled upon this unexpected behavior where when I re-display a new QPixmap in a QLabel. I tried to simplify the code and ended up with the code below. I also attached the video of the behavior.
I provided here a replicable example (It just needs some .jpg file in the same directory):
import sys
import os
import random
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QSizePolicy
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
class AppDemo(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 400, 400)
current_working_dir = os.path.abspath('')
dir_files = os.listdir(current_working_dir)
# Saving .jpg from the dir
self.picture = []
for file in dir_files:
if file.endswith(".jpg"):
self.picture.append(file)
self.label = QLabel()
self.label.setStyleSheet("border: 1px solid black;") # <- for the debugging
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding)
self.label.setPixmap(self.random_picture_selector())
button = QPushButton("Reload Picture")
button.clicked.connect(self.reload_picture)
layout = QVBoxLayout(self)
layout.addWidget(button)
layout.addWidget(self.label)
def reload_picture(self):
self.label.setPixmap(self.random_picture_selector())
def random_picture_selector(self):
rnd_picture = random.choice(self.picture)
pixmap = QPixmap(rnd_picture)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
return pixmap
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()
sys.exit(app.exec_())
Additional Infos:
When simplifying the code I realized that the problem disappears when I removed these following lines. (although I'm not very sure that these part of the code really causes the problem)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
I really have no idea what causes the problem even after looking for the Docs of QPixmap and QLabel.
The problem is caused by the stylesheet border. If you just print the pixmap and label size after setting the pixmap, you'll see that the label width is increased by 2 pixels, which is the sum of the left and right border.
You either remove the border, or you use the contentsRect():
width = self.label.contentsRect().width()
pixmap = pixmap.scaledToWidth(width, Qt.SmoothTransformation)
Read more about the Box Model in the Qt style sheet documentation.
I'm struggling to load an SVG image using PyQt5 QGraphicsSvgItem.renderer().load. MWE:
from PyQt5.QtCore import QByteArray
from PyQt5.QtSvg import QGraphicsSvgItem, QSvgRenderer
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
scene = QGraphicsScene(self)
view = QGraphicsView(scene)
renderer = QSvgRenderer()
self.setCentralWidget(view)
with open('test.svg') as fh:
self.svg_data = fh.read()
self.svg_data = QByteArray(self.svg_data.encode())
self.svg_item = QGraphicsSvgItem()
self.svg_item.setSharedRenderer(renderer)
self.svg_item.renderer().load(self.svg_data)
scene.addItem(self.svg_item)
self.svg_item.setPos(-50, -50)
self.svg_item2 = QGraphicsSvgItem('test2.svg')
scene.addItem(self.svg_item2)
self.svg_item2.setPos(50, 50)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
While test.svg won't be loaded, test2.svg will. See here: Window with a single svg image test.svg is a blue rectangle, test2.svg is a black rectangle.
What am I missing?
(NOTE: I am aware that I can load a svg using either QGraphicsSvgItem('myfile.svg') or QGraphicsSvgItem(QSvgRenderer(my_svg_data)), but I need to update the svg image in an existing object, so these methods wouldn't work for me.)
The problem is that when using the QSvgRenderer to load the .svg then the boundingRect of the QGraphicsSvgItem is not updated so nothing will be drawn. A possible solution is to use passing an empty string to the setElementId method to recalculate the geometry.
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
scene = QGraphicsScene(self)
view = QGraphicsView(scene)
self.setCentralWidget(view)
self.svg_item = QGraphicsSvgItem()
renderer = QSvgRenderer()
self.svg_item.setSharedRenderer(renderer)
with open("test.svg", "rb") as f:
self.svg_item.renderer().load(f.read())
self.svg_item.setElementId("")
scene.addItem(self.svg_item)
self.svg_item.setPos(-50, -50)
self.svg_item2 = QGraphicsSvgItem("test2.svg")
scene.addItem(self.svg_item2)
self.svg_item2.setPos(50, 50)
I have a QPushButton that has a QIcon set. I would like to grayscale the icon's colors until it is hovered over or selected, without writing 2 separate image files for each icon. If use QPushButton.setDisabled(True) the icon's colors do in fact turn to grayscale, so I would like this same behavior being controlled through a enterEvent. Is this at all possible?
Yes, you can do exactly what you described. Enable the button in the enterEvent and disable it in the leaveEvent if it's not checked.
import sys
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
class Button(QPushButton):
def __init__(self):
super().__init__()
self.setCheckable(True)
self.setDisabled(True)
self.setIcon(QIcon('icon.png'))
self.setIconSize(QSize(100, 100))
def enterEvent(self, event):
self.setEnabled(True)
def leaveEvent(self, event):
if not self.isChecked():
self.setDisabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
grid = QGridLayout(w)
grid.addWidget(Button(), 0, 0, Qt.AlignCenter)
w.show()
sys.exit(app.exec_())
I used a green check mark for the icon image. Result:
You also don't need to subclass QPushButton, this will work too.
btn = QPushButton()
btn.setCheckable(True)
btn.setIcon(QIcon('icon.png'))
btn.setIconSize(QSize(100, 100))
btn.setDisabled(True)
btn.enterEvent = lambda _: btn.setEnabled(True)
btn.leaveEvent = lambda _: btn.setEnabled(btn.isChecked())
I was able to display the first image in a label but what I'm looking for is to display all the images in the folder to be displayed side by side in a row and refresh the image automatically if there is any update. How can I create multiple labels and set images on it based on the number of image files in my folder. For example: if my folder contains image for Nicolascage, Tom Hanks, Sandra Bullocks then all their images should be placed in a row and if a new image replace the existing image then it should refresh automatically.
What I achieved so far:
My code:
import cv2,os
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from VideoFaceDetectiondup import vidcaptureface
from PyQt5 import QtCore, QtGui, QtWidgets #works for pyqt5
class MainWindow(QWidget):
def __init__(self, camera_index=0, fps=30):
super().__init__()
self.capture = cv2.VideoCapture(camera_index)
self.dimensions = self.capture.read()[1].shape[1::-1]
scene = QGraphicsScene(self)
pixmap = QPixmap(*self.dimensions)
self.pixmapItem = scene.addPixmap(pixmap)
view = QGraphicsView(self)
view.setScene(scene)
text = QLabel('facecam 1.0', self)
label = QLabel()
pixmap = QPixmap("messigray_1.png")
label.setPixmap(pixmap)
label.show()
# label.setGeometry(QtCore.QRect(1270, 1280, 1200, 1200))
layout = QVBoxLayout(self)
layout.addWidget(view)
layout.addWidget(text)
layout.addWidget(label)
timer = QTimer(self)
timer.setInterval(int(1000/fps))
timer.timeout.connect(self.get_frame)
timer.start()
def get_frame(self):
_, frame = self.capture.read()
frame=vidcaptureface(frame)
image = QImage(frame, *self.dimensions, QImage.Format_RGB888).rgbSwapped()
pixmap = QPixmap.fromImage(image)
self.pixmapItem.setPixmap(pixmap)
app = QApplication([])
win = MainWindow()
win.show()
app.exec()