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())