社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
私信  •  关注

musicamante

musicamante 最近创建的主题
musicamante 最近回复了
3 年前
回复了 musicamante 创建的主题 » pyqt5和python中QToolBox中的动画效果

QToolBox使用QVBoxLayout来显示其内容,添加一个新项实际上会创建两个小部件,并将它们添加到该布局中:

  • 一个自定义的QAbstractButton,用于显示每页的标题并激活它;
  • 包含实际小部件的QScrollArea;

一种可能性是创建一个动画来更新 最大限度 以前和新项目的大小,但有一个问题:按钮连接到一个最终调用的内部函数 setCurrentIndex() 但既然那不是 事实上的 函数,它不能被重写,因此理论上不可能获得鼠标点击和拦截。

幸运的是,QToolBox提供了一个有用的功能,可以帮助我们: itemInserted() 每当添加新项时调用。通过覆盖它,我们不仅可以获得对所有新部件的引用,还可以断开连接 clicked 按钮的信号,并将其连接到我们自己的 setCurrentIndex

为了简化动画过程,我创建了一个helper类,该类保留对按钮、滚动区域和实际小部件的引用,并提供控制滚动区域高度的函数,并根据其当前/未来状态对其进行适当配置。

现在,动画使用从0.0插值到1.0的QVariantAnimation,然后使用该比率根据 目标 身高该高度从工具箱的当前高度开始计算,然后将第一个按钮的大小提示(以及布局间距)乘以页数。通过这种方式,我们可以随时知道滚动区域的目标高度,即使在动画期间调整窗口的大小也是如此。
每当添加和删除项目时,以及调整工具箱大小时,都会调用更新该高度的函数。

动画开始时,需要特别注意计算大小:默认情况下,布局基于其小部件的最小大小提示,滚动区域始终具有最小高度 暗示 72像素。默认情况下,QToolBox通过隐藏上一个项目并显示新项目,立即在页面之间切换,因此大小提示不变,但因为我们要显示 在动画期间同时滚动区域,这将迫使工具箱增大其最小大小。解决方案是将两个滚动区域的最小大小强制为1,以便 minimumSizeHint 将被忽略。
此外,由于显示小部件会增加间距,因此我们需要将隐藏滚动区域的大小减小该量,直到新滚动区域高于间距。

所有这些的唯一问题是,为了防止计算过于复杂,在动画中点击另一个页面必须被忽略,但如果动画足够短,这不应该是一个大问题。

from PyQt5 import QtCore, QtWidgets
from functools import cached_property

class ToolBoxPage(QtCore.QObject):
    destroyed = QtCore.pyqtSignal()
    def __init__(self, button, scrollArea):
        super().__init__()
        self.button = button
        self.scrollArea = scrollArea
        self.widget = scrollArea.widget()
        self.widget.destroyed.connect(self.destroyed)

    def beginHide(self, spacing):
        self.scrollArea.setMinimumHeight(1)
        # remove the layout spacing as showing the new widget will increment
        # the layout size hint requirement
        self.scrollArea.setMaximumHeight(self.scrollArea.height() - spacing)
        # force the scroll bar off if it's not visible before hiding
        if not self.scrollArea.verticalScrollBar().isVisible():
            self.scrollArea.setVerticalScrollBarPolicy(
                QtCore.Qt.ScrollBarAlwaysOff)

    def beginShow(self, targetHeight):
        if self.scrollArea.widget().minimumSizeHint().height() <= targetHeight:
            # force the scroll bar off it will *probably* not required when the
            # widget will be shown
            self.scrollArea.setVerticalScrollBarPolicy(
                QtCore.Qt.ScrollBarAlwaysOff)
        else:
            # the widget will need a scroll bar, but we don't know when;
            # we will show it anyway, even if it's a bit ugly
            self.scrollArea.setVerticalScrollBarPolicy(
                QtCore.Qt.ScrollBarAsNeeded)
        self.scrollArea.setMaximumHeight(0)
        self.scrollArea.show()

    def setHeight(self, height):
        if height and not self.scrollArea.minimumHeight():
            # prevent the layout considering the minimumSizeHint
            self.scrollArea.setMinimumHeight(1)
        self.scrollArea.setMaximumHeight(height)

    def finalize(self):
        # reset the min/max height and the scroll bar policy
        self.scrollArea.setMinimumHeight(0)
        self.scrollArea.setMaximumHeight(16777215)
        self.scrollArea.setVerticalScrollBarPolicy(
            QtCore.Qt.ScrollBarAsNeeded)


class AnimatedToolBox(QtWidgets.QToolBox):
    _oldPage = _newPage = None
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._pages = []

    @cached_property
    def animation(self):
        animation = QtCore.QVariantAnimation(self)
        animation.setDuration(250)
        animation.setStartValue(0.)
        animation.setEndValue(1.)
        animation.valueChanged.connect(self._updateSizes)
        return animation

    @QtCore.pyqtProperty(int)
    def animationDuration(self):
        return self.animation.duration()

    @animationDuration.setter
    def animationDuration(self, duration):
        self.animation.setDuration(max(50, min(duration, 500)))

    @QtCore.pyqtSlot(int)
    @QtCore.pyqtSlot(int, bool)
    def setCurrentIndex(self, index, now=False):
        if self.currentIndex() == index:
            return
        if now:
            if self.animation.state():
                self.animation.stop()
                self._pages[index].finalize()
            super().setCurrentIndex(index)
            return
        elif self.animation.state():
            return
        self._oldPage = self._pages[self.currentIndex()]
        self._oldPage.beginHide(self.layout().spacing())
        self._newPage = self._pages[index]
        self._newPage.beginShow(self._targetSize)
        self.animation.start()

    @QtCore.pyqtSlot(QtWidgets.QWidget)
    @QtCore.pyqtSlot(QtWidgets.QWidget, bool)
    def setCurrentWidget(self, widget):
        for i, page in enumerate(self._pages):
            if page.widget == widget:
                self.setCurrentIndex(i)
                return

    def _index(self, page):
        return self._pages.index(page)

    def _updateSizes(self, ratio):
        if self.animation.currentTime() < self.animation.duration():
            newSize = round(self._targetSize * ratio)
            oldSize = self._targetSize - newSize
            if newSize < self.layout().spacing():
                oldSize -= self.layout().spacing()
            self._oldPage.setHeight(max(0, oldSize))
            self._newPage.setHeight(newSize)
        else:
            super().setCurrentIndex(self._index(self._newPage))
            self._oldPage.finalize()
            self._newPage.finalize()

    def _computeTargetSize(self):
        if not self.count():
            self._targetSize = 0
            return
        l, t, r, b = self.getContentsMargins()
        baseHeight = (self._pages[0].button.sizeHint().height()
            + self.layout().spacing())
        self._targetSize = self.height() - t - b - baseHeight * self.count()

    def _buttonClicked(self):
        button = self.sender()
        for i, page in enumerate(self._pages):
            if page.button == button:
                self.setCurrentIndex(i)
                return

    def _widgetDestroyed(self):
        self._pages.remove(self.sender())

    def itemInserted(self, index):
        button = self.layout().itemAt(index * 2).widget()
        button.clicked.disconnect()
        button.clicked.connect(self._buttonClicked)
        scrollArea = self.layout().itemAt(index * 2 + 1).widget()
        page = ToolBoxPage(button, scrollArea)
        self._pages.insert(index, page)
        page.destroyed.connect(self._widgetDestroyed)
        self._computeTargetSize()

    def itemRemoved(self, index):
        if self.animation.state() and self._index(self._newPage) == index:
            self.animation.stop()
        page = self._pages.pop(index)
        page.destroyed.disconnect(self._widgetDestroyed)
        self._computeTargetSize()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self._computeTargetSize()


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    toolBox = AnimatedToolBox()
    for i in range(8):
        container = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(container)
        for b in range((i + 1) * 2):
            layout.addWidget(QtWidgets.QPushButton('Button {}'.format(b + 1)))
        layout.addStretch()
        toolBox.addItem(container, 'Box {}'.format(i + 1))
    toolBox.show()
    sys.exit(app.exec())