可折叠 QToolButton 在 QScrollArea 中表现怪异(可能是拉伸问题)
Collapsible QToolButton acts weird in QScrollArea ( probably problem with stretch )
我的问题是可折叠 QToolButton
在 QScrollArea
中表现得很奇怪。这不是我使用可折叠 QToolButton
的第一个问题,起初是我的布局没有拉伸,所以我添加了 stretching(addStretch(1)
) 并且它开始工作正常。今天,我尝试将 QScrollArea
添加到布局,然后添加 QToolButtons
及其下方的小部件,同样的问题再次出现。所以我认为这又是拉伸的问题,所以我试图搜索拉伸方式 QScrollArea
并在查看之后尝试设置 setSizePolicy()
、sizeHint()
,但它无法解决问题。有人可以帮我找出问题吗?
更详细的问题解释:当你第一次展开所有可折叠的QToolButton
时没有问题,但是当你关闭它们并再次打开时,从第二个 QToolButton 开始它们开始不打开从最初的几次点击。另外,我不知道是不是问题,但起初当你从 UI 中展开这些按钮时,它们开始前后晃动一点,基本上打开不顺畅。
代码如下:
import random
from PySide2.QtGui import QPixmap, QBrush, QColor, QIcon, QPainterPath, QPolygonF, QPen, QTransform
from PySide2.QtCore import QSize, Qt, Signal, QPointF, QRect, QPoint, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation
from PySide2.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QFrame, \
QSizePolicy, QGraphicsPixmapItem, QApplication, QRubberBand, QMenu, QMenuBar, QTabWidget, QWidget, QPushButton, \
QSlider, QGraphicsPolygonItem, QToolButton, QScrollArea, QLabel
extraDict = {'buttonSetA': ['test'], 'buttonSetB': ['test'], 'buttonSetC': ['test'], 'buttonSetD': ['test']}
class MainWindow(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.create()
def create(self, **kwargs):
main_layout = QVBoxLayout()
tab_widget = QTabWidget()
main_layout.addWidget(tab_widget)
tab_extra = QWidget()
tab_widget.addTab(tab_extra, 'Extra')
tab_main = QWidget()
tab_widget.addTab(tab_main, 'Main')
tab_extra.layout = QVBoxLayout()
tab_extra.setLayout(tab_extra.layout)
scroll = QScrollArea()
content_widget = QWidget()
scroll.setWidget(content_widget)
scroll.setWidgetResizable(True)
#scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
tab_extra.layout.addWidget(scroll)
content_layout = QVBoxLayout(content_widget)
for name in extraDict.keys():
box = CollapsibleBox(name)
content_layout.addWidget(box)
box_layout = QVBoxLayout()
for j in range(8):
label = QLabel("{}".format(j))
color = QColor(*[random.randint(0, 255) for _ in range(3)])
label.setStyleSheet("background-color: {}; color : white;".format(color.name()))
label.setAlignment(Qt.AlignCenter)
box_layout.addWidget(label)
box.setContentLayout(box_layout)
content_layout.addStretch(1)
self.setLayout(main_layout)
class CollapsibleBox(QWidget):
def __init__(self, name):
super(CollapsibleBox, self).__init__()
self.toggle_button = QToolButton(text=name, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QParallelAnimationGroup(self)
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
self.toggle_animation.setDirection(QAbstractAnimation.Forward
if not checked
else QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (self.sizeHint().height() - self.content_area.maximumHeight())
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(500)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(500)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(500, 100, 500, 500)
window.show()
sys.exit(app.exec_())
编辑:
这里是link我录制的一个有问题的小视频,为了画面更清晰。 (问题开始于 0:12)
我注意到只有当我打开所有QToolBox然后从下面一个一个地关闭它然后再次开始打开它们时才会出现问题。
这个问题是因为当可折叠框开始调整大小时,您的鼠标按钮可能仍会被按下,如果按钮由于滚动而移动,它将移动 "outside" 光标位置,导致在按钮区域外接收到按钮释放事件。
这是一个常见的按钮约定,如果用户点击它但将光标移到按钮区域之外并且然后释放按钮,则该按钮不被视为已单击(或勾选)。
此外,可检查按钮在按下时变为 checked(参见 down property),但在释放按钮之前不会 toggled,并且如果它们已经选中,它们在释放时(而不是单击时)变为未选中状态(如 "not down"),但是,如果鼠标按钮在其几何形状内释放,则再次发出切换信号。
如果你仔细看你的视频,你会发现未选中的按钮是灰色的,而选中后它们是浅蓝色的。当您尝试取消选中它们时,它们仍然会收到 pressed
事件(因此动画会按预期工作),但它们仍然会保持按下状态(蓝色),那是因为它们在它们的外部收到了按钮释放事件区域。在第二次尝试展开后尝试单击第二个按钮时,您可以看到颜色差异。
因此,当您再次单击它们时,它们已经关闭,它们会收到 "pressed" 信号,但由于它们已经关闭,因此它们的状态实际上已被检查。
人们会认为使用切换信号就足够了,但这意味着等待鼠标按钮释放(如前所述)并且对于类似的情况并不那么直观,因为用户可能更喜欢立即做出反应鼠标按下,无需等待释放;这是此类可折叠小部件的另一种常见约定。
我能想到的唯一解决方案是创建一个假的释放事件,并在收到按下信号后立即将其发送到按钮。这将使按钮 "think" 鼠标已被释放,从而应用正确的选中状态。
def on_pressed(self):
checked = self.toggle_button.isChecked()
fakeEvent = QtGui.QMouseEvent(
QtCore.QEvent.MouseButtonRelease, self.toggle_button.rect().center(),
QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
QApplication.postEvent(self.toggle_button, fakeEvent)
QMouseEvent的这个class构造函数(还有4 of them)是最简单的,只需要根据按钮矩形设置局部位置即可;使用中心可确保始终收到事件。
最后,postEvent
事件实际上是通过 QApplication 发送给小部件的(通常最好避免将事件直接发送给接收者)。
关于 "shaking" 小部件,这可能是因为您正在使用并行动画来设置内容和容器的高度;虽然从技术上讲这是并行发生的,但我认为问题出在某些时刻这两个尺寸不是 "synchronized",并且布局正在接收(暂时)关于它们的尺寸和提示的不可靠数据,这可能是由于事实上,小部件同时获得最小 和 最大尺寸,内容区域随后会调整大小。
经过一些测试,我可以看出在 setMinimumHeight 和 setMaximumHeight 之间可能发生的事情之间存在一些细微差别。
def __init__(self, name):
# ...
self.toggle_animation.animationAt(0).valueChanged.connect(self.checkSizePre)
self.toggle_animation.animationAt(1).valueChanged.connect(self.checkSizePost)
def checkSizePre(self, value):
self.pre = self.y()
def checkSizePost(self, value):
QApplication.processEvents()
post = self.y()
if self.pre != post:
print('pre {} post {} diff {}'.format(self.pre, post, abs(self.pre - post)))
这会导致 0 到 6 像素之间的差异,这表明设置这些 min/max 值会影响小部件的整体定位。显然,这些值对于手动调整小部件的大小时通常是微不足道的,但由于调整大小事件总是稍微延迟,在这种情况下,整个布局系统没有足够的时间来调整所有内容而不会出现故障。
很遗憾,我现在想不出解决办法,抱歉。
我的问题是可折叠 QToolButton
在 QScrollArea
中表现得很奇怪。这不是我使用可折叠 QToolButton
的第一个问题,起初是我的布局没有拉伸,所以我添加了 stretching(addStretch(1)
) 并且它开始工作正常。今天,我尝试将 QScrollArea
添加到布局,然后添加 QToolButtons
及其下方的小部件,同样的问题再次出现。所以我认为这又是拉伸的问题,所以我试图搜索拉伸方式 QScrollArea
并在查看之后尝试设置 setSizePolicy()
、sizeHint()
,但它无法解决问题。有人可以帮我找出问题吗?
更详细的问题解释:当你第一次展开所有可折叠的QToolButton
时没有问题,但是当你关闭它们并再次打开时,从第二个 QToolButton 开始它们开始不打开从最初的几次点击。另外,我不知道是不是问题,但起初当你从 UI 中展开这些按钮时,它们开始前后晃动一点,基本上打开不顺畅。
代码如下:
import random
from PySide2.QtGui import QPixmap, QBrush, QColor, QIcon, QPainterPath, QPolygonF, QPen, QTransform
from PySide2.QtCore import QSize, Qt, Signal, QPointF, QRect, QPoint, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation
from PySide2.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QFrame, \
QSizePolicy, QGraphicsPixmapItem, QApplication, QRubberBand, QMenu, QMenuBar, QTabWidget, QWidget, QPushButton, \
QSlider, QGraphicsPolygonItem, QToolButton, QScrollArea, QLabel
extraDict = {'buttonSetA': ['test'], 'buttonSetB': ['test'], 'buttonSetC': ['test'], 'buttonSetD': ['test']}
class MainWindow(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.create()
def create(self, **kwargs):
main_layout = QVBoxLayout()
tab_widget = QTabWidget()
main_layout.addWidget(tab_widget)
tab_extra = QWidget()
tab_widget.addTab(tab_extra, 'Extra')
tab_main = QWidget()
tab_widget.addTab(tab_main, 'Main')
tab_extra.layout = QVBoxLayout()
tab_extra.setLayout(tab_extra.layout)
scroll = QScrollArea()
content_widget = QWidget()
scroll.setWidget(content_widget)
scroll.setWidgetResizable(True)
#scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
tab_extra.layout.addWidget(scroll)
content_layout = QVBoxLayout(content_widget)
for name in extraDict.keys():
box = CollapsibleBox(name)
content_layout.addWidget(box)
box_layout = QVBoxLayout()
for j in range(8):
label = QLabel("{}".format(j))
color = QColor(*[random.randint(0, 255) for _ in range(3)])
label.setStyleSheet("background-color: {}; color : white;".format(color.name()))
label.setAlignment(Qt.AlignCenter)
box_layout.addWidget(label)
box.setContentLayout(box_layout)
content_layout.addStretch(1)
self.setLayout(main_layout)
class CollapsibleBox(QWidget):
def __init__(self, name):
super(CollapsibleBox, self).__init__()
self.toggle_button = QToolButton(text=name, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QParallelAnimationGroup(self)
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
self.toggle_animation.setDirection(QAbstractAnimation.Forward
if not checked
else QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (self.sizeHint().height() - self.content_area.maximumHeight())
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(500)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(500)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(500, 100, 500, 500)
window.show()
sys.exit(app.exec_())
编辑:
这里是link我录制的一个有问题的小视频,为了画面更清晰。 (问题开始于 0:12) 我注意到只有当我打开所有QToolBox然后从下面一个一个地关闭它然后再次开始打开它们时才会出现问题。
这个问题是因为当可折叠框开始调整大小时,您的鼠标按钮可能仍会被按下,如果按钮由于滚动而移动,它将移动 "outside" 光标位置,导致在按钮区域外接收到按钮释放事件。
这是一个常见的按钮约定,如果用户点击它但将光标移到按钮区域之外并且然后释放按钮,则该按钮不被视为已单击(或勾选)。
此外,可检查按钮在按下时变为 checked(参见 down property),但在释放按钮之前不会 toggled,并且如果它们已经选中,它们在释放时(而不是单击时)变为未选中状态(如 "not down"),但是,如果鼠标按钮在其几何形状内释放,则再次发出切换信号。
如果你仔细看你的视频,你会发现未选中的按钮是灰色的,而选中后它们是浅蓝色的。当您尝试取消选中它们时,它们仍然会收到 pressed
事件(因此动画会按预期工作),但它们仍然会保持按下状态(蓝色),那是因为它们在它们的外部收到了按钮释放事件区域。在第二次尝试展开后尝试单击第二个按钮时,您可以看到颜色差异。
因此,当您再次单击它们时,它们已经关闭,它们会收到 "pressed" 信号,但由于它们已经关闭,因此它们的状态实际上已被检查。
人们会认为使用切换信号就足够了,但这意味着等待鼠标按钮释放(如前所述)并且对于类似的情况并不那么直观,因为用户可能更喜欢立即做出反应鼠标按下,无需等待释放;这是此类可折叠小部件的另一种常见约定。
我能想到的唯一解决方案是创建一个假的释放事件,并在收到按下信号后立即将其发送到按钮。这将使按钮 "think" 鼠标已被释放,从而应用正确的选中状态。
def on_pressed(self):
checked = self.toggle_button.isChecked()
fakeEvent = QtGui.QMouseEvent(
QtCore.QEvent.MouseButtonRelease, self.toggle_button.rect().center(),
QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
QApplication.postEvent(self.toggle_button, fakeEvent)
QMouseEvent的这个class构造函数(还有4 of them)是最简单的,只需要根据按钮矩形设置局部位置即可;使用中心可确保始终收到事件。
最后,postEvent
事件实际上是通过 QApplication 发送给小部件的(通常最好避免将事件直接发送给接收者)。
关于 "shaking" 小部件,这可能是因为您正在使用并行动画来设置内容和容器的高度;虽然从技术上讲这是并行发生的,但我认为问题出在某些时刻这两个尺寸不是 "synchronized",并且布局正在接收(暂时)关于它们的尺寸和提示的不可靠数据,这可能是由于事实上,小部件同时获得最小 和 最大尺寸,内容区域随后会调整大小。
经过一些测试,我可以看出在 setMinimumHeight 和 setMaximumHeight 之间可能发生的事情之间存在一些细微差别。
def __init__(self, name):
# ...
self.toggle_animation.animationAt(0).valueChanged.connect(self.checkSizePre)
self.toggle_animation.animationAt(1).valueChanged.connect(self.checkSizePost)
def checkSizePre(self, value):
self.pre = self.y()
def checkSizePost(self, value):
QApplication.processEvents()
post = self.y()
if self.pre != post:
print('pre {} post {} diff {}'.format(self.pre, post, abs(self.pre - post)))
这会导致 0 到 6 像素之间的差异,这表明设置这些 min/max 值会影响小部件的整体定位。显然,这些值对于手动调整小部件的大小时通常是微不足道的,但由于调整大小事件总是稍微延迟,在这种情况下,整个布局系统没有足够的时间来调整所有内容而不会出现故障。
很遗憾,我现在想不出解决办法,抱歉。