How to make an Angled arrow style border in PyQt5? - python

How to make an Angled arrow-type border in PyQt QFrame? In My code, I Have two QLabels and respective frames. My aim is to make an arrow shape border on right side of every QFrame.For clear-cut idea, attach a sample picture.
import sys
from PyQt5.QtWidgets import *
class Angle_Border(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Angle Border")
self.lbl1 = QLabel("Python")
self.lbl2 = QLabel("PyQt")
self.frame1 = QFrame()
self.frame1.setProperty("type","1")
self.frame1.setFixedSize(200,50)
self.frame1.setStyleSheet("background-color:red;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;"
"border-top-right-radius:25px solid ; border-bottom-right-radius:25px solid ;")
self.frame2 = QFrame()
self.frame2.setFixedSize(200, 50)
self.frame2.setStyleSheet("background-color:blue;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;"
"border-top:1px solid transparent; border-bottom:1px solid transparent;")
self.frame_outer = QFrame()
self.frame_outer.setFixedSize(800, 60)
self.frame_outer.setStyleSheet("background-color:green;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;")
self.frame1_layout = QHBoxLayout(self.frame1)
self.frame2_layout = QHBoxLayout(self.frame2)
self.frame_outer_layout = QHBoxLayout(self.frame_outer)
self.frame_outer_layout.setContentsMargins(5,0,0,0)
self.frame1_layout.addWidget(self.lbl1)
self.frame2_layout.addWidget(self.lbl2)
self.hbox = QHBoxLayout()
self.layout = QHBoxLayout()
self.hbox.addWidget(self.frame1)
self.hbox.addWidget(self.frame2)
self.hbox.addStretch()
self.hbox.setSpacing(0)
# self.layout.addLayout(self.hbox)
self.frame_outer_layout.addLayout(self.hbox)
self.layout.addWidget(self.frame_outer)
self.setLayout(self.layout)
def main():
app = QApplication(sys.argv)
ex = Angle_Border()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Sample Picture

Since the OP didn't ask for user interaction (mouse or keyboard), a possible solution could use the existing features of Qt, specifically QSS (Qt Style Sheets).
While the currently previously accepted solution does follow that approach, it's not very effective, most importantly because it's basically "static", since it always requires knowing the color of the following item in order to define the "arrow" colors.
This not only forces the programmer to always consider the "sibling" items, but also makes extremely (and unnecessarily) complex the dynamic creation of such objects.
The solution is to always (partially) "redo" the layout and update the stylesheets with the necessary values, which consider the current size (which shouldn't be hardcoded), the following item (if any) and carefully using the layout properties and "spacer" stylesheets based on the contents.
The following code uses a more abstract, dynamic approach, with basic functions that allow adding/insertion and removal of items. It still uses a similar QSS method, but, with almost the same "line count", it provides a simpler and much more intuitive approach, allowing item creation, deletion and modification with single function calls that are much easier to use.
A further benefit of this approach is that implementing "reverse" arrows is quite easy, and doesn't break the logic of the item creation.
Considering all the above, you can create an actual class that just needs basic calls such as addItem() or removeItem().
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ArrowMenu(QWidget):
vMargin = -1
hMargin = -1
def __init__(self, items=None, parent=None):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addStretch()
self.items = []
if isinstance(items, dict):
self.addItems(items.items())
elif items is not None:
self.addItems(items)
def addItems(self, items):
for item in items:
if isinstance(item, str):
self.addItem(item)
else:
self.addItem(*item)
def addItem(self, text, background=None):
self.insertItem(len(self.items), text, background)
def insertItem(self, index, text, background=None):
label = QLabel(text)
if background is None:
background = self.palette().window().color()
background.setAlpha(0)
else:
background = QColor(background)
# human eyes perceive "brightness" in different ways, let's compute
# that value in order to decide a color that has sufficient contrast
# with the background; see https://photo.stackexchange.com/q/10412
r, g, b, a = background.getRgbF()
brightness = r * .3 + g * .59 + b * .11
foreground = 'black' if brightness >= .5 else 'white'
label.setStyleSheet('color: {}; background: {};'.format(
foreground, background.name(background.HexArgb)))
layout = self.layout()
if index < len(self.items):
i = 0
for _label, _spacer, _ in self.items:
if i == index:
i += 1
layout.insertWidget(i * 2, _label)
layout.insertWidget(i * 2 + 1, _spacer)
i += 1
layout.insertWidget(index * 2, label)
spacer = QWidget(objectName='menuArrow')
layout.insertWidget(index * 2 + 1, spacer)
self.items.insert(index, (label, spacer, background))
self.updateItems()
def removeItem(self, index):
label, spacer, background = self.items.pop(index)
label.deleteLater()
spacer.deleteLater()
layout = self.layout()
for i, (label, spacer, _) in enumerate(self.items):
layout.insertWidget(i * 2, label)
layout.insertWidget(i * 2 + 1, spacer)
self.updateItems()
self.updateGeometry()
def updateItems(self):
if not self.items:
return
size = self.fontMetrics().height()
if self.vMargin < 0:
vSize = size * 2
else:
vSize = size + self.vMargin * 2
spacing = vSize / 2
self.setMinimumHeight(vSize)
if self.hMargin >= 0:
labelMargin = self.hMargin * 2
else:
labelMargin = size // 2
it = iter(self.items)
prevBackground = prevSpacer = None
while True:
try:
label, spacer, background = next(it)
label.setContentsMargins(labelMargin, 0, labelMargin, 0)
spacer.setFixedWidth(spacing)
except StopIteration:
background = QColor()
break
finally:
if prevBackground:
if background.isValid():
cssBackground = background.name(QColor.HexArgb)
else:
cssBackground = 'none'
if prevBackground.alpha():
prevBackground = prevBackground.name(QColor.HexArgb)
else:
mid = QColor(prevBackground)
mid.setAlphaF(.5)
prevBackground = '''
qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {}, stop:1 {})
'''.format(
prevBackground.name(QColor.HexArgb),
mid.name(QColor.HexArgb),
)
prevSpacer.setStyleSheet('''
ArrowMenu > .QWidget#menuArrow {{
background: transparent;
border-top: {size}px solid {background};
border-bottom: {size}px solid {background};
border-left: {spacing}px solid {prevBackground};
}}
'''.format(
size=self.height() // 2,
spacing=spacing,
prevBackground=prevBackground,
background=cssBackground
))
prevBackground = background
prevSpacer = spacer
def resizeEvent(self, event):
self.updateItems()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
items = (
('Python', 'green'),
('Will delete', 'chocolate'),
('PyQt5', 'red'),
('Java', 'blue'),
('ASP.Net', 'yellow'),
)
ex = ArrowMenu(items)
ex.show()
QTimer.singleShot(2000, lambda: ex.addItem('New item', 'aqua'))
QTimer.singleShot(5000, lambda: ex.removeItem(1))
sys.exit(app.exec_())
And here is the result:

import sys
from PyQt5.QtWidgets import QWidget,QHBoxLayout,QLabel,QFrame,QApplication,QSizePolicy
from PyQt5.QtCore import Qt
class MyFrame(QWidget):
def __init__(self,base_color,top_color,width,edge,text,text_color):
super().__init__()
self.base_color = base_color
self.top_color = top_color
self.width = width
self.edge = edge
self.text = text
self.text_color = text_color
self.lbl = QLabel()
self.lbl.setText(self.text)
self.lbl.setFixedHeight(self.width*2)
self.lbl.setMinimumWidth((QSizePolicy.MinimumExpanding)+100)
self.lbl.setContentsMargins(0,0,0,0)
self.lbl.setAlignment(Qt.AlignCenter)
self.lbl.setStyleSheet(f"QLabel"
f"{{background-color: {self.base_color};"
f"color:{self.text_color};"
f"font-family:Trebuchet MS;"
f"font-size: 15pt;}}")
self.frame_triangle = QFrame()
self.frame_triangle.setFixedSize(self.width, self.width * 2)
self.frame_triangle.setContentsMargins(0,0,0,0)
self.hbox = QHBoxLayout()
self.hbox.setSpacing(0)
self.hbox.setContentsMargins(0,0,0,0)
self.setLayout(self.hbox)
if self.edge == "right":
self.border = "border-left"
self.hbox.addWidget(self.lbl)
self.hbox.addWidget(self.frame_triangle)
elif self.edge == "left":
self.border = "border-right"
self.hbox.addWidget(self.frame_triangle)
self.hbox.addWidget(self.lbl)
elif self.edge == "none":
self.border = "border-right"
self.hbox.addWidget(self.lbl)
self.lbl.setMinimumWidth((QSizePolicy.MinimumExpanding) + 150)
self.frame_triangle.setStyleSheet(f"QFrame"
f"{{background-color: {self.base_color};"
f"border-top:100px solid {self.top_color};"
f"{self.border}:100px solid {self.base_color};"
f"border-bottom:100px solid {self.top_color};"
f"}}")
class Main_Frame(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Angled Frame")
triangle_size = 50
self.frame1 = MyFrame("lightgrey","green",triangle_size,"right","","lightgrey")
self.frame2 = MyFrame("green","red",triangle_size,"right","Python","white")
self.frame3 = MyFrame("red","blue",triangle_size,"right","PyQt5","white")
self.frame4 = MyFrame("blue","yellow",triangle_size,"right","Java","white")
self.frame5 = MyFrame("yellow","lightgrey",triangle_size,"right","ASP.Net","black")
self.frame_overall = QFrame()
self.frame_overall.setStyleSheet("background-color:lightgrey;")
self.frame_overall.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Maximum)
self.frame_overall_layout = QHBoxLayout(self.frame_overall)
self.frame_overall_layout.setSpacing(0)
# self.frame_overall_layout.addWidget(self.frame1)
self.frame_overall_layout.addWidget(self.frame2)
self.frame_overall_layout.addWidget(self.frame3)
self.frame_overall_layout.addWidget(self.frame4)
self.frame_overall_layout.addWidget(self.frame5)
self.vbox = QHBoxLayout()
self.vbox.setContentsMargins(0,0,0,0)
self.vbox.setSpacing(0)
self.vbox.addStretch()
self.vbox.addWidget(self.frame_overall)
self.vbox.addStretch()
self.setLayout(self.vbox)
def main():
app = QApplication(sys.argv)
ex = Main_Frame()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

It seems that this link can anwser your question. However, I adopt a python version for you.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import QColor, QPainter, QPen, QPainterPath, QBrush
class Angle_Border(QWidget):
def __init__(self, firstButtonX, firstButtonY, buttonWidth, buttonHeight, triangleWidth, labels, colors):
super().__init__()
self.firstButtonX = firstButtonX
self.firstButtonY = firstButtonY
self.buttonWidth = buttonWidth
self.buttonHeight = buttonHeight
self.triangleWidth = triangleWidth
self.labels = labels
self.colors = colors
self.button_lists = []
for i, text_i in enumerate(self.labels):
button_i = QPushButton(text_i, self)
self.button_lists.append(button_i)
button_i.setGeometry(self.firstButtonX + (self.buttonWidth+self.triangleWidth)*i, self.firstButtonY,
self.buttonWidth, self.buttonHeight)
button_i.setStyleSheet("background-color: %s;border-style: outset;border-width: 0px;" % (QColor(self.colors[i]).name()))
# button_i.setStyleSheet("border-style: outset;border-width: 0px;")
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
for i, button_i in enumerate(self.button_lists):
x = button_i.pos().x()
y = button_i.pos().y()
w = button_i.width()
h = button_i.height()
r = QRect(x+w, y, self.triangleWidth, h)
#
# _____p1
# | \ p3
# |_____ /
# p2
point3X = x + w + self.triangleWidth
point3Y = y + h/2
point1X = x + w
point1Y = y
point2X = x + w
point2Y = y + h
path = QPainterPath()
path.moveTo(point1X, point1Y)
path.lineTo(point2X, point2Y)
path.lineTo(point3X, point3Y)
painter.setPen(QPen(Qt.NoPen))
if i != len(self.button_lists) - 1:
painter.fillRect(r, QBrush(self.colors[i+1]))
painter.fillPath(path, QBrush(self.colors[i]))
def main():
app = QApplication(sys.argv)
firstButtonX = 0
firstButtonY = 0
buttonWidth = 50
buttonHeight = 30
triangleWidth = 30
labels = ["step1", "step2", "step3"]
colors = [Qt.red, Qt.blue, Qt.yellow]
ex = Angle_Border(firstButtonX, firstButtonY, buttonWidth, buttonHeight, triangleWidth, labels, colors)
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Which gives:

You can use QTabBar and override its paint event.
For better display of the last tab, we also override the size hint functions in order to have enough space to show the last arrow without clipping it or drawing over the text.
class ArrowTabBar(QTabBar):
def sizeHint(self):
hint = super().sizeHint()
if self.count():
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def minimumTabSizeHint(self, index):
hint = super().minimumTabSizeHint(index)
if index == self.count() - 1:
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def tabSizeHint(self, index):
hint = super().tabSizeHint(index)
if index == self.count() - 1:
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def paintEvent(self, event):
count = self.count()
if not count:
return
qp = QPainter(self)
qp.setRenderHint(qp.Antialiasing)
bottom = self.height()
midY = bottom // 2
midX = midY / 2.5
bottom -= 1
palette = self.palette()
textColor = palette.windowText().color()
normal = palette.mid()
current = palette.dark()
for i in range(count):
rect = self.tabRect(i)
path = QPainterPath()
x = rect.x()
right = rect.right()
if i:
path.moveTo(x - midX, bottom)
path.lineTo(x + midX, midY)
path.lineTo(x - midX, 0)
else:
path.moveTo(x, bottom)
path.lineTo(x, 0)
path.lineTo(right - midX, 0)
path.lineTo(right + midX, midY)
path.lineTo(right - midX, bottom)
if i == self.currentIndex():
qp.setBrush(current)
else:
qp.setBrush(normal)
qp.setPen(Qt.NoPen)
qp.drawPath(path)
qp.setPen(textColor)
qp.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic,
self.tabText(i))
app = QApplication([])
panel = ArrowTabBar()
for i in range(5):
panel.addTab('Item {}'.format(i + 1))
panel.show()
app.exec()

Related

How to call paintEvent in a Qframe PyQt5?

In pyqt5, I want to insert a circular bar in my QFrame named days. For that, I have painted a circular bar but I am unable to call it under a Qframe. In the last two lines, I am trying to call it under a frame but somehow it doesn't work. I would appreciate it greatly if anybody could help!
circular_bar.py
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
shwd = 10
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hatchery System")
self.setStyleSheet("background-color: #2c313c;")
self.resize(900, 500)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
# self.setWindowFlags(Qt.FramelessWindowHint)
self.days_frame = QtWidgets.QFrame(self)
self.days_frame.setGeometry(QtCore.QRect(200, 100, 395, 300))
self.days_frame.setStyleSheet("background-color: #222931;" "border-radius: 10px;" "padding: 5px;")
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(shwd)
shadow.setColor(QColor(Qt.white))
shadow.setOffset(0)
self.days_frame.setGraphicsEffect(shadow)
self.days_label = QtWidgets.QLabel('Days', self.days_frame)
self.days_label.setGeometry(QtCore.QRect(10, 10, 131, 41))
self.days_label.setStyleSheet("color: white;")
font = QtGui.QFont()
font.setPointSize(13)
self.days_label.setFont(font)
# self.days_frame(paintEvent)
def paintEvent(self, e):
self.value = 0
self.width = 200
self.height = 200
self.progress_width = 10
self.progress_rounded_cap = True
self.max_value = 100
self.progress_color = 0xff79c6
# Text
self.enable_text = True
self.font_family = "Segoe UI"
self.font_size = 12
self.suffix = "%"
self.text_color = 0xff79c6
# BG
self.enable_bg = True
self.bg_color = 0x44475a
# SET PROGRESS PARAMETERS
width = self.width - self.progress_width
height = self.height - self.progress_width
margin = self.progress_width / 2
value = self.value * 360 / self.max_value
# PAINTER
paint = QPainter(self)
paint.begin(self)
paint.setRenderHint(QPainter.Antialiasing) # remove pixelated edges
paint.setFont(QFont(self.font_family, self.font_size))
# CREATE RECTANGLE
rect = QRect(0, 0, self.width, self.height)
paint.setPen(Qt.NoPen)
paint.drawRect(rect)
# PEN
pen = QPen()
pen.setWidth(self.progress_width)
# Set Round Cap
if self.progress_rounded_cap:
pen.setCapStyle(Qt.RoundCap)
# ENABLE BG
if self.enable_bg:
pen.setColor(QColor(self.bg_color))
paint.setPen(pen)
paint.drawArc(margin, margin, width, height, 0, 360 * 16)
# CREATE ARC / CIRCULAR PROGRESS
pen.setColor(QColor(self.progress_color))
paint.setPen(pen)
paint.drawArc(margin, margin, width, height, -90 * 16, -value * 16)
# CREATE TEXT
if self.enable_text:
pen.setColor(QColor(self.text_color))
paint.setPen(pen)
paint.drawText(rect, Qt.AlignCenter, f"{self.value}{self.suffix}")
# END
paint.end()
# tring to call paintEvent
self.days_frame(paintEvent(self, e))
#self.days_frame.paintEvent(self, e)
app = QApplication([])
mw = MainWindow()
mw.show()
app.exec_()
output
Updated Code
progress.py
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class CircularProgress(QtWidgets.QWidget):
def __init__(self):
super().__init__()
# CUSTOM PROPERTIES
self.value = 0
self.width = 200
self.height = 200
self.progress_width = 10
self.progress_rounded_cap = True
self.max_value = 100
self.progress_color = 0xff79c6
# Text
self.enable_text = True
self.font_family = "Segoe UI"
self.font_size = 12
self.suffix = "%"
self.text_color = 0xff79c6
# BG
self.enable_bg = True
self.bg_color = 0x44475a
# SET DEFAULT SIZE WITHOUT LAYOUT
self.resize(self.width, self.height)
self.show()
# ADD DROPSHADOW
def add_shadow(self, enable):
if enable:
self.shadow = QGraphicsDropShadowEffect(self)
self.shadow.setBlurRadius(15)
self.shadow.setXOffset(0)
self.shadow.setYOffset(0)
self.shadow.setColor(QColor(0, 0, 0, 80))
self.setGraphicsEffect(self.shadow)
# SET VALUE
def set_value(self, value):
self.value = value
self.repaint() # Render progress bar after change value
# PAINT EVENT (DESIGN YOUR CIRCULAR PROGRESS HERE)
def paintEvent(self, e):
# SET PROGRESS PARAMETERS
width = self.width - self.progress_width
height = self.height - self.progress_width
margin = self.progress_width / 2
value = self.value * 360 / self.max_value
# PAINTER
paint = QPainter()
paint.begin(self)
paint.setRenderHint(QPainter.Antialiasing) # remove pixelated edges
paint.setFont(QFont(self.font_family, self.font_size))
# CREATE RECTANGLE
rect = QRect(0, 0, self.width, self.height)
paint.setPen(Qt.NoPen)
paint.drawRect(rect)
# PEN
pen = QPen()
pen.setWidth(self.progress_width)
# Set Round Cap
if self.progress_rounded_cap:
pen.setCapStyle(Qt.RoundCap)
# ENABLE BG
if self.enable_bg:
pen.setColor(QColor(self.bg_color))
paint.setPen(pen)
paint.drawArc(margin, margin, width, height, 0, 360 * 16)
# CREATE ARC / CIRCULAR PROGRESS
pen.setColor(QColor(self.progress_color))
paint.setPen(pen)
paint.drawArc(margin, margin, width, height, -90 * 16, -value * 16)
# CREATE TEXT
if self.enable_text:
pen.setColor(QColor(self.text_color))
paint.setPen(pen)
paint.drawText(rect, Qt.AlignCenter, f"{self.value}{self.suffix}")
# END
paint.end()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = CircularProgress()
sys.exit(app.exec_())
main.py
from circularBar import CircularProgress
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.days_frame = QtWidgets.QFrame(self)
self.days_frame.setGeometry(QtCore.QRect(845, 150, 395, 300))
self.days_frame.setStyleSheet("background-color: #222931;" "border-radius: 10px;" "padding: 5px;")
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(shwd)
shadow.setColor(QColor(Qt.white))
shadow.setOffset(0)
self.days_frame.setGraphicsEffect(shadow)
self.days_label = QtWidgets.QLabel('Days', self.days_frame)
self.days_label.setGeometry(QtCore.QRect(10, 10, 131, 41))
self.days_label.setStyleSheet("color: white;")
font = QtGui.QFont
font.setPointSize(13)
self.days_label.setFont(font)
# self.days_frame
self.progress = CircularProgress(self.days_frame)
app = QApplication([])
mw = MainWindow()
mw.show()
app.exec_()
Error:

Pyside6 Animated Rectangle trouble

I'm trying to make fading looped rectangle area. I used base code from here
Just decided expand it.
Its just blinking rectangle, but I need smooth fade-in and fade-out effects. So I decided to make method which will calulate new opacity percent and set it to painter. But it doesnt work in cycle.
This is my class now
class HighlightRect(QFrame):
board_width = 400 # width of frame
board_height = 400 #height of frame
def __init__(self, parent, x, y, width=50, height=50, blink_speed=1000):
super().__init__(parent)
self.blink_speed = blink_speed
self.opacity_timer = self.blink_speed
self.board_height = self.parent().height()
self.board_width = self.parent().width()
self.square_height = height
self.square_width = width
self.highlight_x = x
self.highlight_y = y
#self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.timer_draw = QtCore.QTimer(self)
self.timer_draw.timeout.connect(self.draw)
self.timer_draw.start(self.blink_speed)
self.color = QtCore.Qt.red
self.is_draw = False
self.x_apple = 0
self.y_apple = 0
self.draw()
def blink(self, painter):
self.color = QtCore.Qt.red
while self.opacity_timer >= 0:
self.opacity_timer -= 1 / 10 # просто подбор
percents = round(int(self.opacity_timer / self.blink_speed * 100)/100, 1)
print(percents)
painter.setOpacity(percents)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
print ("Paint Event?")
if self.is_draw == True:
print ("Draw")
#self.color = QtCore.Qt.red
self.blink_thread = threading.Thread(name='background', target=lambda: self.blink(painter))
self.blink_thread.start()
else:
self.opacity_timer = self.blink_speed
print ("Do not draw")
self.color = QtCore.Qt.transparent
threading.SystemExit = SystemExit
painter.setPen(self.color)
painter.drawRect(self.rect)
def draw(self):
self.is_draw = not self.is_draw
self.rect = QRect(self.highlight_x, self.highlight_y, self.square_width, self.square_height)
self.update()
Changind of opacity inside blink function but outside while loop works as well, but its static. No changes. Changing opacity in loop isn't work.
Whats wrong?
Maybe somewhere here is another more correct way to get what I want?
One possible solution is to create a QProperty that handles opacity and then use QPropertyAnimation to make the change smooth.
import random
import sys
from PySide6.QtCore import Property, Signal, QPropertyAnimation, QRect, Qt
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import QFrame, QApplication
class Board(QFrame):
rect_opacity_changed = Signal(name="rectOpacityChanged")
def __init__(self, parent=None):
super(Board, self).__init__(parent)
self._rect_opacity = 1.0
self._rect = QRect(0, 0, 50, 50)
self._opacity_animation = QPropertyAnimation(
targetObject=self, propertyName=b"rect_opacity", duration=3000
)
for p, v in ((0.0, 0.0), (0.3, 1.0), (0.7, 1.0), (1.0, 0.0)):
self._opacity_animation.setKeyValueAt(p, v)
self._opacity_animation.finished.connect(self.change)
self.change()
#Property(float, notify=rect_opacity_changed)
def rect_opacity(self):
return self._rect_opacity
#rect_opacity.setter
def rect_opacity(self, opacity):
self._rect_opacity = opacity
self.rect_opacity_changed.emit()
self.update()
def change(self):
x = random.randint(0, self.width() - self._rect.width())
y = random.randint(0, self.height() - self._rect.height())
self._rect.moveTo(x, y)
self._opacity_animation.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(self.rect_opacity)
painter.setPen(Qt.red)
painter.drawRect(self._rect)
def main():
app = QApplication([])
board = Board()
board.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

How can I add pyqt5 buttons on top of each other?

I am trying to create a basic 25 key keyboard in pyqt, I have laid out the 15 white keys but am struggling with how to add the 10 remaining black keys,
This is how I made my keys
from PyQt5.QtWidgets import QApplication, QPushButton
app = QApplication([])
top_win = QWidget()
set_color(top_win, Qt.cyan)
top_win.setAutoFillBackground(True)
top_win.show()
top_win.resize(1920,1080)
top_win.setWindowTitle("Synth-01")
top_vlayout = QVBoxLayout()
top_win.setLayout(top_vlayout)
keyboard = QWidget()
keyboard.setMaximumWidth(1410)
top_vlayout.addWidget(keyboard)
keyboard_layout = QHBoxLayout()
keyboard.setAutoFillBackground(True)
keyboard.setLayout(keyboard_layout)
for i in range(15):
name = "key_" + str(i)
name = QPushButton()
name.setMaximumWidth(94)
name.setMaximumHeight(349)
keyboard_layout.addWidget(name)
I now want to add the black keys inbetween like this
In this case, it is most preferable to use QGraphicsScene with QGraphicsItem, for this case I will use svg:
from PyQt5 import QtCore, QtGui, QtWidgets, QtSvg
class PianoKey(QtWidgets.QGraphicsRectItem):
def __init__(self, black=False, rect = QtCore.QRectF(), parent=None):
super(PianoKey, self).__init__(rect, parent)
self.m_pressed = False
self.m_selectedBrush = QtGui.QBrush()
self.m_brush = QtGui.QBrush(QtCore.Qt.black) if black else QtGui.QBrush(QtCore.Qt.white)
self.m_black = black
def setPressedBrush(self, brush):
self.m_selectedBrush = brush
def paint(self, painter, option, widget):
rendered = QtSvg.QSvgRenderer("key.svg")
black_pen = QtGui.QPen(QtCore.Qt.black, 1)
gray_pen = QtGui.QPen(QtGui.QBrush(QtCore.Qt.gray), 1,
QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
if self.m_pressed:
if self.m_selectedBrush.style() != QtCore.Qt.NoBrush:
painter.setBrush(self.m_selectedBrush)
else:
painter.setBrush(QtWidgets.QApplication.palette().highlight())
else:
painter.setBrush(self.m_brush);
painter.setPen(black_pen)
painter.drawRoundedRect(self.rect(), 15, 15, QtCore.Qt.RelativeSize)
if self.m_black:
rendered.render(painter, self.rect())
else:
points = [
QtCore.QPointF(self.rect().left()+1.5, self.rect().bottom()-1),
QtCore.QPointF(self.rect().right()-1, self.rect().bottom()-1),
QtCore.QPointF(self.rect().right()-1, self.rect().top()+1)
]
painter.setPen(gray_pen)
painter.drawPolyline(QtGui.QPolygonF(points))
def mousePressEvent(self, event):
self.m_pressed = True
self.update()
super(PianoKey, self).mousePressEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
self.m_pressed = False
self.update()
super(PianoKey, self).mouseReleaseEvent(event)
KEYWIDTH, KEYHEIGHT = 18, 72
class PianoKeyBoard(QtWidgets.QGraphicsView):
def __init__(self, num_octaves=2, parent=None):
super(PianoKeyBoard, self).__init__(parent)
self.initialize()
self.m_numOctaves = num_octaves
scene = QtWidgets.QGraphicsScene(QtCore.QRectF(0, 0, KEYWIDTH * self.m_numOctaves * 7, KEYHEIGHT), self)
self.setScene(scene)
numkeys = self.m_numOctaves * 12
for i in range(numkeys):
octave = i//12*7
j = i % 12
if j >= 5: j += 1
if j % 2 == 0:
x = (octave + j/2)*KEYWIDTH
key = PianoKey(rect=QtCore.QRectF(x, 0, KEYWIDTH, KEYHEIGHT), black=False)
else:
x = (octave + j//2) * KEYWIDTH + KEYWIDTH * 6//10 + 1
key = PianoKey(rect=QtCore.QRectF(x, 0, KEYWIDTH * 8//10 - 1, KEYHEIGHT * 6//10 ), black=True)
key.setZValue(1)
key.setPressedBrush(QtWidgets.QApplication.palette().highlight())
self.scene().addItem(key)
def initialize(self):
self.setAttribute(QtCore.Qt.WA_InputMethodEnabled, False)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QtWidgets.QGraphicsView.MinimalViewportUpdate)
self.setRenderHints(QtGui.QPainter.Antialiasing|
QtGui.QPainter.TextAntialiasing |
QtGui.QPainter.SmoothPixmapTransform)
self.setOptimizationFlag(QtWidgets.QGraphicsView.DontClipPainter, True)
self.setOptimizationFlag(QtWidgets.QGraphicsView.DontSavePainterState, True)
self.setOptimizationFlag(QtWidgets.QGraphicsView.DontAdjustForAntialiasing, True)
self.setBackgroundBrush(QtWidgets.QApplication.palette().base())
def resizeEvent(self, event):
super(PianoKeyBoard, self).resizeEvent(event)
self.fitInView(self.scene().sceneRect(), QtCore.Qt.KeepAspectRatio)
def sizeHint(self):
return self.mapFromScene(self.sceneRect()).boundingRect().size()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(QtWidgets.QLabel("Piano Keyboard", alignment=QtCore.Qt.AlignCenter))
lay.addWidget(PianoKeyBoard())
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
The complete code + key.svg can be found on this link

PyQt keep aspect ratio fixed

I'm working on a PyQt5 GUI, so far, I've just had experience with python scripts and did not delve into creating user interfaces.
The GUI will have to be used on different screens (maybe also some old 4:3 ratio screens) and will need to look nice in different sizes.
Now, my approach to make my life easier was to enforce a fixed aspect ratio of the window and resize the different elements according to window size.
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.form_widget = FormWidget(self)
self.setCentralWidget(self.form_widget)
self.resize(200, 400)
self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
self.sizePolicy.setHeightForWidth(True)
self.setSizePolicy(self.sizePolicy)
def heightForWidth(self, width):
return width * 2
class FormWidget(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
def resizeEvent(self, event):
f = self.font()
temp = event.size().height()
f.setPixelSize(temp / 16)
self.setFont(f)
return super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Resizing the elements according to window size works fine, but window aspect ratio is not kept at all.
I copied this approach with heightForWidth from old PyQt4 threads. Doesn't this approach work anymore in PyQt5? Am I missing something?
If I understood your question, you should try using a layout inside the main window.
I did this:
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.central_widget = QtWidgets.QWidget()
self.central_layout = QtWidgets.QVBoxLayout()
self.setCentralWidget(self.central_widget)
self.central_widget.setLayout(self.central_layout)
# Lets create some widgets inside
self.label = QtWidgets.QLabel()
self.list_view = QtWidgets.QListView()
self.push_button = QtWidgets.QPushButton()
self.label.setText('Hi, this is a label. And the next one is a List View :')
self.push_button.setText('Push Button Here')
# Lets add the widgets
self.central_layout.addWidget(self.label)
self.central_layout.addWidget(self.list_view)
self.central_layout.addWidget(self.push_button)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
If you resize the window, the widgets inside it get resized.
First, answered by Marc and codeling in this question, heightForWidth is only supported for QGraphicsLayout's subclasses.
Second, how to make a fixed aspect ratio window (or top-level widget) in qt (or pyqt) is a question that have been asked for years. However, as far as I know, there is no standard way of doing so, and it is something surprisingly hard to achieve. In short, my way of doing this is use Qt.FramelessWindowHint to create a frameless window without system move and resize function, and implement custom move and resize.
Explain important mechanism:
move:
In mousePressEvent, keep the place where we last clicked on the widget(the draggable area).
In mouseMoveEvent, calculate the distance between the last clicked point and the current mouse location. Move the window according to this distance.
resize:
Find the increase or decrease step size of width and height by dividing the minimum width and height of the window by their highest common factor.
Use the step size to increase or decrease the window size to keep the aspect ratio.
A screenshot to show that it can resize according to the aspect ratio.
The following code should works with both PyQt5 and Pyside2.
from PyQt5.QtCore import Qt, QRect, QPoint, QEvent
from PyQt5.QtWidgets import (QLabel, QMainWindow, QApplication, QSizePolicy,
QVBoxLayout, QWidget, QHBoxLayout, QPushButton)
from enum import Enum
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowFlags(Qt.FramelessWindowHint)
self.createCostumTitleBar()
self.setContentsMargins(0, 0, 0, 0)
self.central = QWidget()
self.central.setStyleSheet("background-color: #f8ecdf")
self.centralLayout = QVBoxLayout()
self.central.setLayout(self.centralLayout)
self.centralLayout.addWidget(
self.costumsystemmenu, alignment=Qt.AlignTop)
self.centralLayout.setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(self.central)
# Set the minimum size to avoid window being resized too small.
self.setMinimumSize(300, 400)
self.minheight = self.minimumHeight()
self.minwidth = self.minimumWidth()
self.resize(300, 400)
# make sure your minium size have the same aspect ratio as the step.
self.stepY = 4
self.stepX = 3
# install the event filter on this window.
self.installEventFilter(self)
self.grabarea.installEventFilter(self)
self.cursorpos = CursorPos.DEFAULT
self.iswindowpress = False
def createCostumTitleBar(self):
self.costumsystemmenu = QWidget()
self.costumsystemmenu.setStyleSheet("background-color: #ccc")
self.costumsystemmenu.setContentsMargins(0, 0, 0, 0)
self.costumsystemmenu.setMinimumHeight(30)
self.grabarea = QLabel("")
self.grabarea.setStyleSheet("background-color: #ccc")
self.grabarea.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Preferred)
titlebarlayout = QHBoxLayout()
titlebarlayout.setContentsMargins(11, 11, 11, 11)
titlebarlayout.setSpacing(0)
self.closeButton = QPushButton("X")
self.closeButton.setSizePolicy(
QSizePolicy.Minimum, QSizePolicy.Preferred)
self.closeButton.clicked.connect(self.close)
self.costumsystemmenu.setLayout(titlebarlayout)
titlebarlayout.addWidget(self.grabarea)
titlebarlayout.addWidget(self.closeButton, alignment=Qt.AlignRight)
self.istitlebarpress = False
def eventFilter(self, object, event):
# The eventFilter() function must return true if the event
# should be filtered, (i.e. stopped); otherwise it must return false.
# https://doc.qt.io/qt-5/qobject.html#eventFilter
# check if the object is the mainwindow.
if object == self:
if event.type() == QEvent.HoverMove:
if not self.iswindowpress:
self.setCursorShape(event)
return True
elif event.type() == QEvent.MouseButtonPress:
self.iswindowpress = True
# Get the position of the cursor and map to the global coordinate of the widget.
self.globalpos = self.mapToGlobal(event.pos())
self.origingeometry = self.geometry()
return True
elif event.type() == QEvent.MouseButtonRelease:
self.iswindowpress = False
return True
elif event.type() == QEvent.MouseMove:
if self.cursorpos != CursorPos.DEFAULT and self.iswindowpress:
self.resizing(self.globalpos, event,
self.origingeometry, self.cursorpos)
return True
else:
return False
elif object == self.grabarea:
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton and self.iswindowpress == False:
self.oldpos = event.globalPos()
self.oldwindowpos = self.pos()
self.istitlebarpress = True
return True
elif event.type() == QEvent.MouseButtonRelease:
self.istitlebarpress = False
return True
elif event.type() == QEvent.MouseMove:
if (self.istitlebarpress):
distance = event.globalPos()-self.oldpos
newwindowpos = self.oldwindowpos + distance
self.move(newwindowpos)
return True
else:
return False
else:
return False
# Change the cursor shape when the cursor is over different part of the window.
def setCursorShape(self, event, handlersize=11):
rect = self.rect()
topLeft = rect.topLeft()
topRight = rect.topRight()
bottomLeft = rect.bottomLeft()
bottomRight = rect.bottomRight()
# get the position of the cursor
pos = event.pos()
# make the resize handle include some space outside the window,
# can avoid user move too fast and loss the handle.
# top handle
if pos in QRect(QPoint(topLeft.x()+handlersize, topLeft.y()-2*handlersize),
QPoint(topRight.x()-handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.TOP
# bottom handle
elif pos in QRect(QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize),
QPoint(bottomRight.x()-handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.BOTTOM
# right handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()+handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.RIGHT
# left handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()+handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.LEFT
# topRight handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()-2*handlersize),
QPoint(topRight.x()+2*handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.TOPRIGHT
# topLeft handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()-2*handlersize),
QPoint(topLeft.x()+handlersize, topLeft.y()+handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.TOPLEFT
# bottomRight handle
elif pos in QRect(QPoint(bottomRight.x()-handlersize, bottomRight.y()-handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.BOTTOMRIGHT
# bottomLeft handle
elif pos in QRect(QPoint(bottomLeft.x()-2*handlersize, bottomLeft.y()-handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()+2*handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.BOTTOMLEFT
# Default is the arrow cursor.
else:
self.setCursor(Qt.ArrowCursor)
self.cursorpos = CursorPos.DEFAULT
def resizing(self, originpos, event, geo, cursorpos):
newpos = self.mapToGlobal(event.pos())
# find the distance between new and old cursor position.
dist = newpos - originpos
# calculate the steps to grow or srink.
if cursorpos in [CursorPos.TOP, CursorPos.BOTTOM,
CursorPos.TOPRIGHT,
CursorPos.BOTTOMLEFT, CursorPos.BOTTOMRIGHT]:
steps = dist.y()//self.stepY
elif cursorpos in [CursorPos.LEFT, CursorPos.TOPLEFT, CursorPos.RIGHT]:
steps = dist.x()//self.stepX
# if the distance moved is too stort, grow or srink by 1 step.
if steps == 0:
steps = -1 if dist.y() < 0 or dist.x() < 0 else 1
oldwidth = geo.width()
oldheight = geo.height()
oldX = geo.x()
oldY = geo.y()
if cursorpos in [CursorPos.TOP, CursorPos.TOPRIGHT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX
newY = oldY + (steps * self.stepY)
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos in [CursorPos.BOTTOM, CursorPos.RIGHT, CursorPos.BOTTOMRIGHT]:
width = oldwidth + steps * self.stepX
height = oldheight + steps * self.stepY
self.resize(width, height)
elif cursorpos in [CursorPos.LEFT, CursorPos.BOTTOMLEFT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos == CursorPos.TOPLEFT:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY + steps * self.stepY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
else:
pass
# cursor position
class CursorPos(Enum):
TOP = 1
BOTTOM = 2
RIGHT = 3
LEFT = 4
TOPRIGHT = 5
TOPLEFT = 6
BOTTOMRIGHT = 7
BOTTOMLEFT = 8
DEFAULT = 9
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Finally, I'd like to give special thanks to the authors and editors of this question, GLHF, DRPK, Elad Joseph, and SimoN SavioR. Without their contribution to the community, it wouldn't be possible to come up with this answer.

qt stackwidget animation; previous widget disappears

I am trying to use the animation tools (QPropertyAnimation) in Qt to animate the changing of a QStackedWidget. Unfortunately, there are two issues:
The previous widget disappears (doesn't animate)
The next widget does not change position until visible
Any ideas?
Python 2.7, PyQt4, Win 7, Win 10, OpenSuse
Complete Example
import sys
from PyQt4 import QtCore, QtGui
COLOR_LIST = ['red','blue','green']
ANIMATION_SPEED = 2000
def make_callback(func, *param):
'''
Helper function to make sure lambda functions are cached and not lost.
'''
return lambda: func(*param)
class App(QtGui.QMainWindow):
def __init__(self, app, parent=None):
QtGui.QMainWindow.__init__(self, parent)
# reference to qapp instance
self.app = app
self.animating = False
self.stack_animation = None
self.resize(QtCore.QSize(500,200))
# widgets
self.mainwidget = QtGui.QWidget()
self.setCentralWidget(self.mainwidget)
self.listwidget = QtGui.QListWidget()
self.listwidget.addItems(COLOR_LIST)
self.listwidget.itemSelectionChanged.connect(self.change_color)
self.stackedwidget = QtGui.QStackedWidget()
for color in COLOR_LIST:
widget = QtGui.QWidget()
widget.setStyleSheet('QWidget{'
' background-color: '+color+';'
'}')
widget.setObjectName(color)
self.stackedwidget.addWidget(widget)
# layouts
self.hlayout = QtGui.QHBoxLayout(self.mainwidget)
self.mainwidget.setLayout(self.hlayout)
self.hlayout.addWidget(self.listwidget)
self.hlayout.addWidget(self.stackedwidget)
def change_color(self):
new_color = str(self.listwidget.currentItem().text())
old_color = str(self.stackedwidget.currentWidget().objectName())
old_index = self.stackedwidget.currentIndex()
new_index = 0
for i in range(self.stackedwidget.count()):
widget = self.stackedwidget.widget(i)
if new_color == str(widget.objectName()):
new_index = i
break
print('Changing from:', old_color, old_index,
'To:', new_color, new_index)
self.animate(old_index, new_index)
def animate(self, from_, to, direction='vertical'):
""" animate changing of qstackedwidget """
# check to see if already animating
if self.animating and self.stack_animation is not None:
self.stack_animation.stop()
from_widget = self.stackedwidget.widget(from_)
to_widget = self.stackedwidget.widget(to)
# get from geometry
width = from_widget.frameGeometry().width()
height = from_widget.frameGeometry().height()
# offset
# bottom to top
if direction == 'vertical' and from_ < to:
offsetx = 0
offsety = height
# top to bottom
elif direction == 'vertical' and from_ > to:
offsetx = 0
offsety = -height
elif direction == 'horizontal' and from_ < to:
offsetx = width
offsety = 0
elif direction == 'horizontal' and from_ > to:
offsetx = -width
offsety = 0
else:
return
# move to widget and show
# set the geometry of the next widget
to_widget.setGeometry(0 + offsetx, 0 + offsety, width, height)
to_widget.show()
to_widget.lower()
to_widget.raise_()
# animate
# from widget
animnow = QtCore.QPropertyAnimation(from_widget, "pos")
animnow.setDuration(ANIMATION_SPEED)
animnow.setEasingCurve(QtCore.QEasingCurve.InOutQuint)
animnow.setStartValue(
QtCore.QPoint(0,
0))
animnow.setEndValue(
QtCore.QPoint(0 - offsetx,
0 - offsety))
# to widget
animnext = QtCore.QPropertyAnimation(to_widget, "pos")
animnext.setDuration(ANIMATION_SPEED)
animnext.setEasingCurve(QtCore.QEasingCurve.InOutQuint)
animnext.setStartValue(
QtCore.QPoint(0 + offsetx,
0 + offsety))
animnext.setEndValue(
QtCore.QPoint(0,
0))
# animation group
self.stack_animation = QtCore.QParallelAnimationGroup()
self.stack_animation.addAnimation(animnow)
self.stack_animation.addAnimation(animnext)
self.stack_animation.finished.connect(
make_callback(self.animate_stacked_widget_finished,
from_, to)
)
self.stack_animation.stateChanged.connect(
make_callback(self.animate_stacked_widget_finished,
from_, to)
)
self.animating = True
self.stack_animation.start()
def animate_stacked_widget_finished(self, from_, to):
""" cleanup after animation """
self.stackedwidget.setCurrentIndex(to)
from_widget = self.stackedwidget.widget(from_)
from_widget.hide()
from_widget.move(0, 0)
self.animating = False
if __name__ == '__main__':
qapp = QtGui.QApplication(sys.argv)
app = App(qapp)
app.show()
qapp.exec_()
qapp.deleteLater()
sys.exit()
The QtCore.QParallelAnimationGroup.stateChanged signal is called when the animation starts, so it was calling my animate_stacked_widget_finished method and hiding the from_widget.
Still need to catch this event to handle when the animation is finished. Just need to add an if statement to check the state of the QParallelAnimationGroup.
Replace the animate_stacked_widget_finished with:
def animate_stacked_widget_finished(self, from_, to):
""" cleanup after animation """
if self.stack_animation.state() == QtCore.QAbstractAnimation.Stopped:
self.stackedwidget.setCurrentIndex(to)
from_widget = self.stackedwidget.widget(from_)
from_widget.hide()
from_widget.move(0, 0)
self.animating = False

Categories