可以为QToolboxes设置动画效果吗?我有一个工具箱与2页,并希望动画之间的切换。(上下滑动)使用currentIndex,我只需在0和1之间切换,所以使用QPropertyanimation动画是不可能的。还有别的办法吗?
发布于 2022-01-19 04:28:05
QToolBox使用一个QVBoxLayout来显示其内容,而添加一个新项实际上会创建两个小部件并将它们添加到该布局中:
一种可能是创建一个动画来更新上一项和新项的最大大小,但是有一个问题:按钮连接到一个最终调用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())https://stackoverflow.com/questions/70746992
复制相似问题