首页 > 解决方案 > 可折叠 QToolButton 在 QScrollArea 中的行为很奇怪(可能与拉伸有关)

问题描述

我的问题是可折叠QToolButtonQScrollArea. 这不是我的第一个可折叠问题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_())

编辑 :

这是我录制的有问题的小视频的链接,以获得更清晰的画面。(问题从0:12开始)我注意到只有当我打开所有QToolBox然后从下面一个一个关闭它然后再次开始打开它们时才会出现问题。

标签: pythonpyside2

解决方案


问题在于,当可折叠框开始调整大小时,您的鼠标按钮可能仍会被按下,如果按钮由于滚动而移动,它将被移动到“光标位置之外”,从而导致按钮释放事件在按钮区域外被接收。

按钮的一个常见约定是,如果用户单击它但将光标移到按钮区域之外然后释放按钮,则该按钮不被视为已单击(或选中)。
此外,可检查按钮在按下时变为选中状态(参见down 属性),但在释放按钮之前不会切换,如果它们已经选中,则在释放时(而不是单击时)它们将变为未选中状态(如“未按下”),但是,如果鼠标按钮在其几何形状内释放,则会再次发出切换信号。

如果您仔细查看您的视频,您会发现未选中的按钮是灰色的,而选中时它们具有浅蓝色阴影。当您尝试取消选中它们时,它们仍然会收到pressed事件(因此动画按预期工作),但它们仍然保持按下状态(蓝色),这是因为它们在其区域之外收到了按钮释放事件。在第二次尝试展开第二个按钮后尝试单击第二个按钮时,您可以看到色差。因此,当您再次单击它们时,它们已经处于关闭状态,它们会收到“按下”信号,但由于它们已经处于关闭状态,因此它们的状态实际上已被检查。

有人会认为使用切换信号就足够了,但这意味着等待鼠标按钮释放(如前所述),并且对于类似的情况并不那么直观,因为用户可能更喜欢立即对鼠标按下做出反应,而无需等待发布;这是这种可折叠小部件的另一个常见约定。

我能想到的唯一解决方案是创建一个假释放事件,并在收到按下信号后立即将其发送到按钮。这将使按钮“认为”鼠标已被释放,从而应用正确的选中状态。

    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的这个类构造函数(有4个)是最简单的,只需要根据按钮矩形设置本地位置即可;使用中心可确保始终收到事件。
最后,postEvent事件实际上是通过 QApplication 发送到小部件(通常最好避免将事件直接发送到接收器)。


关于“摇晃”小部件,这可能是因为您使用了并行动画来设置内容和容器的高度;虽然从技术上讲这是并行发生的,但我认为问题出在某些时刻,在此期间两种尺寸没有“同步”,并且布局正在接收(暂时)关于它们的尺寸和提示的不可靠数据,可能是由于事实小部件获得最小最大尺寸,然后调整内容区域的大小。

经过一些测试后,我可以看出 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 像素之间变化,这表明设置这些最小值/最大值会影响小部件的整体定位。显然,当手动调整小部件的大小时,这些值通常是微不足道的,但由于调整大小事件总是稍微延迟,在这种情况下,整个布局系统没有足够的时间来调整所有内容而不会出现故障。

不幸的是,我现在想不出解决方案,抱歉。


推荐阅读