PySide2 复合小部件悬停效果

Posted

技术标签:

【中文标题】PySide2 复合小部件悬停效果【英文标题】:PySide2 composite widget Hover Effect 【发布时间】:2019-09-21 15:07:42 【问题描述】:

如何在鼠标指针进入或离开 QListWidgetItem 时为自定义小部件内的组合元素的位置、比例或任何其他属性设置动画? (见下图)

还有没有更好的方法来管理 ListWidgetItem 周围的空间? item_widget.sizeHint() 提供了不需要的额外空间,这就是我向 setSizeHint 添加硬编码值的原因。

ProductMainWindow.py

from PySide2 import QtCore, QtGui, QtWidgets
import productThumbnailWidget
import sys
sys.path.append('E:/code')

class prodMainWindowUI(QtWidgets.QMainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.setupUi(self)    
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.productListWidget = QtWidgets.QListWidget(self.centralwidget)
        self.productListWidget.setObjectName("productListWidget")
        self.productListWidget.setViewMode(QtWidgets.QListWidget.IconMode)
        self.productListWidget.setIconSize(QtCore.QSize(256,256))
        self.productListWidget.setResizeMode(QtWidgets.QListWidget.Adjust)
        self.productListWidget.setMovement(QtWidgets.QListWidget.Static) # disable drag and drop
        self.productListWidget.setGridSize(QtCore.QSize(256 + 5, 256 + 5))

        self.verticalLayout.addWidget(self.productListWidget)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

        for i in range(6):
            item = QtWidgets.QListWidgetItem(self.productListWidget)
            item_widget = productThumbnailWidget.productThumbWidget()
            #item.setSizeHint(item_widget.sizeHint())
            item.setSizeHint(QtCore.QSize(256,256))
            self.productListWidget.addItem(item)
            self.productListWidget.setItemWidget(item, item_widget)

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))


app = QtWidgets.QApplication(sys.argv)
prodUI = prodMainWindowUI()
prodUI.show()
sys.exit(app.exec_())

productThumbnailWidget

from PySide2 import QtCore, QtGui, QtWidgets

class productThumbWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(productThumbWidget, self).__init__(parent)
        self.setObjectName("Form")
        self.resize(256, 256)
        self.setMinimumSize(QtCore.QSize(256, 256))
        self.setMaximumSize(QtCore.QSize(256, 256))

        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setSpacing(0)
        self.verticalLayout.setObjectName("verticalLayout")

        self.frame = QtWidgets.QFrame()
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")

        self.thumbnailLabel = QtWidgets.QLabel("", self.frame)
        self.thumbnailLabel.setObjectName("thumbnailLabel")
        self.thumbnailLabel.setScaledContents(False)
        self.thumbnailLabel.setGeometry(QtCore.QRect(0, 0, 256, 256))
        self.thumbnailLabel.setMinimumSize(QtCore.QSize(256, 256))
        self.thumbnailLabel.setMaximumSize(QtCore.QSize(256, 256))
        self.thumbnailLabel.setPixmap(QtGui.QPixmap("E:/code/android.png"))    

        self.backgroundLabel = QtWidgets.QLabel("", self.frame)
        self.backgroundLabel.setObjectName("backgroundLabel")
        self.backgroundLabel.setGeometry(QtCore.QRect(0, 206, 256, 50))
        self.backgroundLabel.setMinimumSize(QtCore.QSize(256, 50))
        self.backgroundLabel.setMaximumSize(QtCore.QSize(256, 50))        
        self.backgroundLabel.setStyleSheet("QLabel#backgroundLabel\n"
"    background: #32353B;\n"
"")        

        self.titleLabel = QtWidgets.QLabel("Title", self.frame)
        self.titleLabel.setObjectName("titleLabel")
        self.titleLabel.setGeometry(QtCore.QRect(10, 218, 246, 25))
        self.titleLabel.setMinimumSize(QtCore.QSize(246, 25))
        self.titleLabel.setMaximumSize(QtCore.QSize(246, 25))
        font = QtGui.QFont()
        font.setFamily("SF Pro Display")
        font.setPointSize(16)
        font.setWeight(75)
        font.setBold(True)
        self.titleLabel.setFont(font)
        self.titleLabel.setStyleSheet("QLabel#titleLabel \n"
"    color: #FFFFFF;\n"
"")
        self.verticalLayout.addWidget(self.frame)
        self.setLayout(self.verticalLayout)

输出

【问题讨论】:

【参考方案1】:

解决方案是使用 QPropertyAnimation 为矩形的位置设置动画,并使用事件 QEvent::Enter 和 QEvent::Leave: 来激活动画

import shiboken2
from PySide2 import QtCore, QtGui, QtWidgets


class RectangleHoverEffect(QtCore.QObject):
    def __init__(self, rectangle, parent):
        super().__init__(parent)
        if not isinstance(rectangle, QtWidgets.QWidget):
            raise TypeError(f"rectangle must be a QWidget")
        if rectangle.parent() is None:
            raise ValueError(f"rectangle must have a parent")
        self.m_rectangle = rectangle
        self.m_rectangle.parent().installEventFilter(self)
        self.m_animation = QtCore.QPropertyAnimation(
            self,
            targetObject=self.m_rectangle,
            propertyName=b"pos",
            duration=300,
            easingCurve=QtCore.QEasingCurve.OutQuad,
        )

    def eventFilter(self, obj, event):
        if shiboken2.isValid(self.m_rectangle):
            if self.m_rectangle.parent() is obj:
                y0 = self.m_rectangle.parent().height()
                y1 = self.m_rectangle.parent().height() - self.m_rectangle.height()

                if event.type() == QtCore.QEvent.Enter:
                    self._start_animation(y0, y1)
                elif event.type() == QtCore.QEvent.Leave:
                    self._start_animation(y1, y0)
        return super().eventFilter(obj, event)

    def _start_animation(self, y0, y1):
        self.m_animation.setStartValue(QtCore.QPoint(0, y0))
        self.m_animation.setEndValue(QtCore.QPoint(0, y1))
        self.m_animation.start()


class ThumbWidget(QtWidgets.QFrame):
    def __init__(self, title, pixmap, parent=None):
        super().__init__(parent)
        self.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.setFrameShadow(QtWidgets.QFrame.Raised)

        pixmap_label = QtWidgets.QLabel(pixmap=pixmap, scaledContents=True)
        title_label = QtWidgets.QLabel(title)
        title_label.setStyleSheet("""color: #FFFFFF""")
        font = QtGui.QFont()
        font.setFamily("SF Pro Display")
        font.setPointSize(16)
        font.setWeight(75)
        font.setBold(True)
        title_label.setFont(font)

        background_label = QtWidgets.QLabel(pixmap_label)
        background_label.setStyleSheet("background: #32353B;")
        background_label.setFixedSize(self.width(), 50)
        background_label.move(0, self.height())

        background_lay = QtWidgets.QVBoxLayout(background_label)
        background_lay.addWidget(title_label)

        self.setFixedSize(256, 256)
        lay = QtWidgets.QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)
        lay.addWidget(pixmap_label)

        effect = RectangleHoverEffect(background_label, self)


class ProductMainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.product_listwidget = QtWidgets.QListWidget(
            viewMode=QtWidgets.QListWidget.IconMode,
            iconSize=QtCore.QSize(256, 256),
            resizeMode=QtWidgets.QListWidget.Adjust,
            movement=QtWidgets.QListWidget.Static,
        )
        self.product_listwidget.setGridSize(QtCore.QSize(256 + 5, 256 + 5))

        for i in range(6):
            item = QtWidgets.QListWidgetItem()
            item_widget = ThumbWidget(f"Title i", QtGui.QPixmap("E:/code/android.png"))
            item.setSizeHint(QtCore.QSize(256, 256))
            self.product_listwidget.addItem(item)
            self.product_listwidget.setItemWidget(item, item_widget)

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        lay = QtWidgets.QVBoxLayout(central_widget)
        lay.addWidget(self.product_listwidget)

        self.resize(960, 480)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = ProductMainWindow()
    w.show()
    sys.exit(app.exec_())

【讨论】:

在向 QListWidget 填充 2000-3000 个项目时,RAM 使用率变高(直到我关闭应用程序才下降)。如何解决这个问题?

以上是关于PySide2 复合小部件悬停效果的主要内容,如果未能解决你的问题,请参考以下文章

在 PyQt5/Pyside2 中动态添加小部件到流布局

PySide2 在设计师创建的小部件上绘制

仅为 QTabWidget 中的选项卡显示工具提示,而不是整个小部件

子类化并覆盖 PySide2 小部件方法;我在哪里可以找到参考资料?

GTKMM/C++11:如何从其他小部件中创建自定义复合小部件?

简单的 Qt 小部件来绘制线条和形状(如 tkinter 画布)?