首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >QToolBox在pyqt5和python中的动画效果

QToolBox在pyqt5和python中的动画效果
EN

Stack Overflow用户
提问于 2022-01-17 20:13:13
回答 1查看 281关注 0票数 0

可以为QToolboxes设置动画效果吗?我有一个工具箱与2页,并希望动画之间的切换。(上下滑动)使用currentIndex,我只需在0和1之间切换,所以使用QPropertyanimation动画是不可能的。还有别的办法吗?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-01-19 04:28:05

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

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

一种可能是创建一个动画来更新上一项和新项的最大大小,但是有一个问题:按钮连接到一个最终调用setCurrentIndex()的内部函数,但是由于它不是一个虚拟函数,所以不能覆盖它,所以理论上不可能获得鼠标点击和拦截。

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

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

现在,动画使用一个从0.0到1.0内插的QVariantAnimation,然后使用该比率根据目标高度计算这两个小部件之间的比例。从工具箱的当前高度开始计算该高度,然后减去第一个按钮的大小提示(和布局间隔)乘以页数。通过这种方式,我们可以随时知道滚动区域的目标高度是什么,甚至在动画期间调整窗口大小时也是如此。

每当添加和删除项时,以及在调整工具箱大小时,都会调用更新高度的函数。

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

另外,由于显示一个小部件会增加一个间距,所以我们需要将隐藏滚动区域的大小减少那么多,直到新的滚动区域比间距高为止。

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

代码语言:javascript
复制
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())
票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/70746992

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档