[Python] Unexpected behavior of drawing widgets after animation of one over the other PySide6

Discussão em 'Python' iniciado por Stack, Agosto 22, 2025.

  1. Stack

    Stack Membro Participativo

    I am using PySide6 to create a GUI and currently I am trying to have an animated sliding bar, like a burger menu, on the left side which when hovered upon will cover the QListView in the middle.

    Currently my approach is a QGridView that consists of a QWidget on the left, subclassed in my ExpandingSidebar class, which has a vertical layout of QToolButtons that span the whole column. On the next column of the QGridView I have a QListView that I populate with gibberish as of now.

    I have implemented an animation in which when you hover over the buttons, the width of the QListView goes to 0, and the ToolButtons display their icon as well as their text temporarily extended at the width of the QListView, until the mouse is moved away. My desired behavior is that after moving the mouse away the QToolButtons reset to their original state and the text vanishes. Currently the text remains and it is elided by the again expanding QListView. And I also get some undefined behavior if I play around with it too much.

    I am generating my UI using the QtDesigner and compiling to .py files using PySide6 This is the result of the compiled MainWindow file:

    # -*- coding: utf-8 -*-

    ################################################################################
    ## Form generated from reading UI file 'main_window.ui'
    ##
    ## Created by: Qt User Interface Compiler version 6.9.1
    ##
    ## WARNING! All changes made in this file will be lost when recompiling UI file!
    ################################################################################

    from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
    from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
    from PySide6.QtWidgets import (QApplication, QGraphicsView, QGridLayout, QHBoxLayout,
    QListView, QMainWindow, QPushButton, QSizePolicy,
    QSpacerItem, QStatusBar, QVBoxLayout, QWidget)

    from gui.expanding_sidebar import ExpandingSidebar
    from gui.sidebar_button import SidebarButton
    import icons_rc

    class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
    if not MainWindow.objectName():
    MainWindow.setObjectName(u"MainWindow")
    MainWindow.resize(840, 687)
    MainWindow.setStyleSheet(u"")
    MainWindow.setUnifiedTitleAndToolBarOnMac(False)
    self.centralwidget = QWidget(MainWindow)
    self.centralwidget.setObjectName(u"centralwidget")
    sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
    sizePolicy.setHorizontalStretch(0)
    sizePolicy.setVerticalStretch(0)
    sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
    self.centralwidget.setSizePolicy(sizePolicy)
    self.gridLayout = QGridLayout(self.centralwidget)
    self.gridLayout.setObjectName(u"gridLayout")
    self.gridLayout.setContentsMargins(0, 0, 0, 0)
    self.graphicsView = QGraphicsView(self.centralwidget)
    self.graphicsView.setObjectName(u"graphicsView")
    sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
    sizePolicy1.setHorizontalStretch(0)
    sizePolicy1.setVerticalStretch(0)
    sizePolicy1.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth())
    self.graphicsView.setSizePolicy(sizePolicy1)

    self.gridLayout.addWidget(self.graphicsView, 2, 2, 1, 2)

    self.widget = ExpandingSidebar(self.centralwidget)
    self.widget.setObjectName(u"widget")
    self.verticalLayout = QVBoxLayout(self.widget)
    self.verticalLayout.setObjectName(u"verticalLayout")
    self.toolButton = SidebarButton(self.widget)
    self.toolButton.setObjectName(u"toolButton")
    sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
    sizePolicy2.setHorizontalStretch(0)
    sizePolicy2.setVerticalStretch(0)
    sizePolicy2.setHeightForWidth(self.toolButton.sizePolicy().hasHeightForWidth())
    self.toolButton.setSizePolicy(sizePolicy2)
    icon = QIcon()
    icon.addFile(u":/icons/keyboard.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.toolButton.setIcon(icon)
    self.toolButton.setAutoExclusive(True)
    self.toolButton.setArrowType(Qt.NoArrow)

    self.verticalLayout.addWidget(self.toolButton)

    self.toolButton_3 = SidebarButton(self.widget)
    self.toolButton_3.setObjectName(u"toolButton_3")
    sizePolicy2.setHeightForWidth(self.toolButton_3.sizePolicy().hasHeightForWidth())
    self.toolButton_3.setSizePolicy(sizePolicy2)
    icon1 = QIcon()
    icon1.addFile(u":/icons/screen.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.toolButton_3.setIcon(icon1)
    self.toolButton_3.setChecked(False)
    self.toolButton_3.setAutoExclusive(True)
    self.toolButton_3.setAutoRaise(False)

    self.verticalLayout.addWidget(self.toolButton_3)

    self.toolButton_2 = SidebarButton(self.widget)
    self.toolButton_2.setObjectName(u"toolButton_2")
    sizePolicy2.setHeightForWidth(self.toolButton_2.sizePolicy().hasHeightForWidth())
    self.toolButton_2.setSizePolicy(sizePolicy2)
    icon2 = QIcon()
    icon2.addFile(u":/icons/scanner-image.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.toolButton_2.setIcon(icon2)
    self.toolButton_2.setAutoExclusive(True)

    self.verticalLayout.addWidget(self.toolButton_2)

    self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)

    self.verticalLayout.addItem(self.verticalSpacer)


    self.gridLayout.addWidget(self.widget, 2, 0, 1, 1)

    self.titleBar = QHBoxLayout()
    self.titleBar.setSpacing(0)
    self.titleBar.setObjectName(u"titleBar")
    self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)

    self.titleBar.addItem(self.horizontalSpacer)

    self.minimizePushButton = QPushButton(self.centralwidget)
    self.minimizePushButton.setObjectName(u"minimizePushButton")
    icon3 = QIcon()
    icon3.addFile(u":/icons/minimize.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.minimizePushButton.setIcon(icon3)
    self.minimizePushButton.setIconSize(QSize(16, 16))
    self.minimizePushButton.setFlat(True)

    self.titleBar.addWidget(self.minimizePushButton)

    self.maximizePushButton = QPushButton(self.centralwidget)
    self.maximizePushButton.setObjectName(u"maximizePushButton")
    icon4 = QIcon()
    icon4.addFile(u":/icons/maximize.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.maximizePushButton.setIcon(icon4)
    self.maximizePushButton.setIconSize(QSize(16, 16))
    self.maximizePushButton.setFlat(True)

    self.titleBar.addWidget(self.maximizePushButton)

    self.closePushButton = QPushButton(self.centralwidget)
    self.closePushButton.setObjectName(u"closePushButton")
    icon5 = QIcon()
    icon5.addFile(u":/icons/close.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
    self.closePushButton.setIcon(icon5)
    self.closePushButton.setIconSize(QSize(16, 16))
    self.closePushButton.setFlat(True)

    self.titleBar.addWidget(self.closePushButton)


    self.gridLayout.addLayout(self.titleBar, 0, 0, 1, 3)

    self.listView = QListView(self.centralwidget)
    self.listView.setObjectName(u"listView")
    sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
    sizePolicy3.setHorizontalStretch(0)
    sizePolicy3.setVerticalStretch(0)
    sizePolicy3.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth())
    self.listView.setSizePolicy(sizePolicy3)
    self.listView.setMaximumSize(QSize(16777215, 16777215))

    self.gridLayout.addWidget(self.listView, 2, 1, 1, 1)

    self.gridLayout.setColumnStretch(0, 1)
    self.gridLayout.setColumnStretch(1, 2)
    self.gridLayout.setColumnStretch(2, 10)
    MainWindow.setCentralWidget(self.centralwidget)
    self.statusbar = QStatusBar(MainWindow)
    self.statusbar.setObjectName(u"statusbar")
    MainWindow.setStatusBar(self.statusbar)

    self.retranslateUi(MainWindow)

    QMetaObject.connectSlotsByName(MainWindow)
    # setupUi

    def retranslateUi(self, MainWindow):
    MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
    self.toolButton.setText(QCoreApplication.translate("MainWindow", u"Keyboard", None))
    self.toolButton_3.setText(QCoreApplication.translate("MainWindow", u"Screen", None))
    self.toolButton_2.setText(QCoreApplication.translate("MainWindow", u"Scanner", None))
    self.minimizePushButton.setText("")
    self.maximizePushButton.setText("")
    self.closePushButton.setText("")
    # retranslateUi


    Then my main

    # main.py
    import sys

    from PySide6.QtGui import QStandardItemModel, QStandardItem, QColor
    from PySide6.QtWidgets import QApplication, QMainWindow, QGraphicsDropShadowEffect
    from PySide6.QtCore import Qt, QPoint

    # Import your generated UI file
    from ui_main_window import Ui_MainWindow
    import qt_material


    class MainWindow(QMainWindow):
    def __init__(self):
    super().__init__()
    self.ui = Ui_MainWindow()
    self.ui.setupUi(self)
    self.setupSidebar()
    self.setWindowFlags(Qt.FramelessWindowHint)
    self.setAttribute(Qt.WA_TranslucentBackground)
    self.ui.minimizePushButton.clicked.connect(self.showMinimized)
    self.ui.maximizePushButton.clicked.connect(self.toggleMaximizeRestore)
    self.ui.closePushButton.clicked.connect(self.close)
    self.dragging = False
    self.drag_position = QPoint()
    shadow = QGraphicsDropShadowEffect(self)
    shadow.setBlurRadius(20)
    shadow.setColor(QColor(0, 0, 0, 150))
    shadow.setOffset(0, 0)
    self.centralWidget().setGraphicsEffect(shadow)

    def setupSidebar(self):
    sidebar = self.ui.widget
    self.list_view = self.ui.listView
    self.model = QStandardItemModel()
    self.list_view.setModel(self.model)

    sidebar.addButton(self.ui.toolButton)
    sidebar.addButton(self.ui.toolButton_3)
    sidebar.addButton(self.ui.toolButton_2)
    sidebar.set_list_view(self.list_view)

    self.populate_list_fast()
    self.list_view.show()

    def toggleMaximizeRestore(self):
    if self.isMaximized():
    self.showNormal()
    else:
    self.showMaximized()

    def mousePressEvent(self, event):
    if event.button() == Qt.LeftButton:
    self.dragging = True
    self.drag_position = (
    event.globalPosition().toPoint() - self.frameGeometry().topLeft()
    )
    event.accept()

    def mouseMoveEvent(self, event):
    if event.buttons() == Qt.LeftButton and self.dragging:
    self.move(event.globalPosition().toPoint() - self.drag_position)
    event.accept()

    def mouseReleaseEvent(self, event):
    if event.button() == Qt.LeftButton:
    self.dragging = False
    event.accept()

    def populate_list_fast(self):
    items = []
    for i in range(100): # Large number of items
    item = QStandardItem(f"Item {i}")
    self.model.appendRow(item)
    items.append(item)
    print("items ready")


    if __name__ == "__main__":
    app = QApplication(sys.argv)
    qt_material.apply_stylesheet(app, "dark_amber.xml")
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


    then my SidebarButton subclass for the QToolButtons:

    # gui/sidebar_button.py
    from PySide6.QtWidgets import QToolButton
    from PySide6.QtCore import Qt, QSize, Signal


    class SidebarButton(QToolButton):
    mouseEntered = Signal()
    mouseLeft = Signal()

    def __init__(self, parent=None):
    super().__init__(parent)
    self._full_text = ""
    self._collapsed = True
    self.setIconSize(QSize(24, 24))
    self.setToolButtonStyle(Qt.ToolButtonIconOnly)
    self.setFixedHeight(50)
    self.setCheckable(True)
    self._original_text = None

    def setCollapsed(self, collapsed):
    self._collapsed = collapsed
    if collapsed:
    self.setToolButtonStyle(Qt.ToolButtonIconOnly)
    super().setText("")
    else:
    self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
    print(f"Set text to {self._original_text}")
    super().setText(self._original_text)

    def setText(self, text):
    if self._original_text is None:
    self._original_text = text
    super().setText(text)

    def enterEvent(self, event):
    self.mouseEntered.emit()

    def leaveEvent(self, event):
    self.mouseLeft.emit()


    and lastly the subclass of the QWidget that has the QToolButtons that should animate:

    from typing import List

    from PySide6.QtWidgets import QWidget, QListView, QToolButton
    from PySide6.QtCore import (
    QPropertyAnimation,
    QEasingCurve,
    Qt,
    Slot,
    QTimer,
    QParallelAnimationGroup,
    )

    from src.gui.sidebar_button import SidebarButton


    class ExpandingSidebar(QWidget):
    """
    Subclass of QWidget that handles the sliding left column of the application
    """

    def __init__(self, parent=None):
    super().__init__(parent)
    self._collapsed = True
    self.left_animation: QPropertyAnimation = QPropertyAnimation(
    self, b"maximumWidth"
    )
    self.right_animation: QPropertyAnimation = None
    self._buttons: List[SidebarButton] = []
    self._listview: QListView = None

    def set_list_view(self, list_view):
    self._listview = list_view

    def addButton(self, button):
    """Add a button to the sidebar and track it for expand/collapse
    behavior"""
    self._buttons.append(button)
    button.setCollapsed(self._collapsed)
    button.mouseEntered.connect(self.handleButtonHover)
    button.mouseLeft.connect(self.handleButtonLeave)

    @Slot()
    def handleButtonHover(self):
    """Handle button hover event - expand sidebar if collapsed"""
    if self._collapsed:
    self.expand()

    @Slot()
    def handleButtonLeave(self):
    """Handle button leave event - prepare to collapse if needed"""
    if not self._collapsed:
    self.collapseWithDelay()

    def expand(self):
    self.left_animation = QPropertyAnimation(self, b"maximumWidth")
    self.right_animation = QPropertyAnimation(self._listview, b"maximumWidth")
    self.left_animation.setDuration(300)
    self.right_animation.setDuration(300)
    self.left_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
    self.right_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)

    self.left_animation.setStartValue(self.width())
    self.left_animation.setEndValue(self.width() + self._listview.width())
    self.right_animation.setStartValue(self._listview.width())
    self.right_animation.setEndValue(0)

    self.left_animation.start()
    self.right_animation.start()

    for button in self._buttons:
    button.setCollapsed(False)

    self._collapsed = False

    def collapseWithDelay(self):
    QTimer.singleShot(100, self.collapse)

    def collapse(self):
    if not self._collapsed and not self.underMouse():
    self.left_animation = QPropertyAnimation(self, b"maximumWidth")
    self.right_animation = QPropertyAnimation(self._listview, b"maximumWidth")
    self.left_animation.setDuration(300)
    self.right_animation.setDuration(300)
    self.left_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
    self.right_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
    self.left_animation.setStartValue(self.width())
    self.left_animation.setEndValue(50)
    self.right_animation.setStartValue(self._listview.width())
    self.right_animation.setEndValue(400)

    self.left_animation.start()
    self.right_animation.start()
    self.right_animation.finished.connect(self.reset_buttons)

    def reset_buttons(self):
    print("Called to reset")
    for button in self._buttons:
    button.setCollapsed(True)
    self._collapsed = True

    def isCollapsed(self):
    return self._collapsed


    After going through the Qt wiki, I am unsure what is exactly at fault, my intuition tells me that is an issue of repaints and the order at which each element is shown, but I am too unfamiliar with the inner workings of Qt to address this. I have tried to batch my animation together in a group. To not use signals and do the button collapse manually like the expansion, and neither approach gave me the required result. Currently the animation finishes and the width of the buttons widget is reset, but their text remains, and through debugging I have concluded that the button.setCollapsed(True) is never actually called. Which I have no idea why. Please forgive the mix-up of CamelCase and snake-case, this will all be fixed after I reach a working solution

    Continue reading...

Compartilhe esta Página