可折叠的QToolButton在QScrollArea中行为异常(可能是拉伸问题)

问题描述 投票:0回答:1

我的问题是,可折叠的QToolButtonQScrollArea中表现得很奇怪。折叠QToolButton并不是我的第一个问题,起初这是我的布局没有拉伸,所以我添加了Stretch(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,然后从下面一个接一个地关闭它,然后开始再次打开它们时,才会出现问题。

python pyside2
1个回答
2
投票

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

这是按钮的常见约定,如果用户单击它,但将光标移到按钮区域之外,然后然后释放按钮,则该按钮不被视为单击(或选中)。同样,可按下的按钮在按下时变为已选中(请参阅down property),但直到释放按钮后才变为toggled,并且如果它们已被选中,则变为未选中(如“不按下”) )释放时(不是单击时),但是,如果在其几何形状内释放鼠标按钮,则再次发出切换信号。

[如果您仔细地观看视频,您会发现未选中的按钮是灰色的,而选中时它们具有浅蓝色。当您尝试取消选中它们时,它们仍然会收到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 of them)是最简单的,您只需要基于按钮矩形设置局部位置即可;使用中心可确保始终收到该事件。最后,使用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像素之间的差异,这表明设置这些最小/最大值会影响小部件的整体位置。显然,对于手动调整窗口小部件的大小,这些值通常微不足道,但是由于调整大小事件总是会稍有延迟,因此在这种情况下,整个布局系统没有足够的时间来调整所有内容而不会出现小故障。

不幸的是,我现在想不出解决方案,对不起。


0
投票

尝试:

import sys
from PyQt5.QtCore import (QRectF, Qt, QPropertyAnimation, pyqtProperty, 
                          QPoint, QParallelAnimationGroup, QEasingCurve)
from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen
from PyQt5.QtWidgets import (QLabel, QWidget, QVBoxLayout, QApplication,
                             QLineEdit, QPushButton)


class BubbleLabel(QWidget):

    BackgroundColor = QColor(195, 195, 195)
    BorderColor = QColor(150, 150, 150)

    def __init__(self, *args, **kwargs):
        text = kwargs.pop("text", "")
        super(BubbleLabel, self).__init__(*args, **kwargs)
        self.setWindowFlags(
            Qt.Window | Qt.Tool | Qt.FramelessWindowHint | 
            Qt.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint)
        # Set minimum width and height
        self.setMinimumWidth(200)
        self.setMinimumHeight(58)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        layout = QVBoxLayout(self)
        # Top left and bottom right margins (16 below because triangles are included)
        layout.setContentsMargins(8, 8, 8, 16)
        self.label = QLabel(self)
        layout.addWidget(self.label)
        self.setText(text)
        # Get screen height and width
        self._desktop = QApplication.instance().desktop()

    def setText(self, text):
        self.label.setText(text)

    def text(self):
        return self.label.text()

    def stop(self):
        self.hide()
        self.animationGroup.stop()
        self.close()

    def show(self):
        super(BubbleLabel, self).show()
        # Window start position
        startPos = QPoint(
            self._desktop.screenGeometry().width() - self.width() - 100,
            self._desktop.availableGeometry().height() - self.height())
        endPos = QPoint(
            self._desktop.screenGeometry().width() - self.width() - 100,
            self._desktop.availableGeometry().height() - self.height() * 3 - 5)
        self.move(startPos)
        # Initialization animation
        self.initAnimation(startPos, endPos)

    def initAnimation(self, startPos, endPos):
        # Transparency animation
        opacityAnimation = QPropertyAnimation(self, b"opacity")
        opacityAnimation.setStartValue(1.0)
        opacityAnimation.setEndValue(0.0)
        # Set the animation curve
        opacityAnimation.setEasingCurve(QEasingCurve.InQuad)
        opacityAnimation.setDuration(4000)  
        # Moving up animation
        moveAnimation = QPropertyAnimation(self, b"pos")
        moveAnimation.setStartValue(startPos)
        moveAnimation.setEndValue(endPos)
        moveAnimation.setEasingCurve(QEasingCurve.InQuad)
        moveAnimation.setDuration(5000)  
        # Parallel animation group (the purpose is to make the two animations above simultaneously)
        self.animationGroup = QParallelAnimationGroup(self)
        self.animationGroup.addAnimation(opacityAnimation)
        self.animationGroup.addAnimation(moveAnimation)
        # Close window at the end of the animation
        self.animationGroup.finished.connect(self.close)  
        self.animationGroup.start()

    def paintEvent(self, event):
        super(BubbleLabel, self).paintEvent(event)
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)  # Antialiasing

        rectPath = QPainterPath()                     # Rounded Rectangle
        triPath = QPainterPath()                      # Bottom triangle

        height = self.height() - 8                    # Offset up 8
        rectPath.addRoundedRect(QRectF(0, 0, self.width(), height), 5, 5)
        x = self.width() / 5 * 4
        triPath.moveTo(x, height)                     # Move to the bottom horizontal line 4/5
        # Draw triangle
        triPath.lineTo(x + 6, height + 8)
        triPath.lineTo(x + 12, height)

        rectPath.addPath(triPath)                     # Add a triangle to the previous rectangle

        # Border brush
        painter.setPen(QPen(self.BorderColor, 1, Qt.SolidLine,
                            Qt.RoundCap, Qt.RoundJoin))
        # Background brush
        painter.setBrush(self.BackgroundColor)
        # Draw shape
        painter.drawPath(rectPath)
        # Draw a line on the bottom of the triangle to ensure the same color as the background
        painter.setPen(QPen(self.BackgroundColor, 1,
                            Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
        painter.drawLine(x, height, x + 12, height)

    def windowOpacity(self):
        return super(BubbleLabel, self).windowOpacity()

    def setWindowOpacity(self, opacity):
        super(BubbleLabel, self).setWindowOpacity(opacity)

    # Since the opacity property is not in QWidget, you need to redefine one
    opacity = pyqtProperty(float, windowOpacity, setWindowOpacity)


class TestWidget(QWidget):

    def __init__(self, *args, **kwargs):
        super(TestWidget, self).__init__(*args, **kwargs)
        layout = QVBoxLayout(self)
        self.msgEdit = QLineEdit(self, returnPressed=self.onMsgShow)
        self.msgButton = QPushButton("Display content", self, clicked=self.onMsgShow)
        layout.addWidget(self.msgEdit)
        layout.addWidget(self.msgButton)

    def onMsgShow(self):
        msg = self.msgEdit.text().strip()
        if not msg:
            return
        if hasattr(self, "_blabel"):
            self._blabel.stop()
            self._blabel.deleteLater()
            del self._blabel
        self._blabel = BubbleLabel()
        self._blabel.setText(msg)
        self._blabel.show()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = TestWidget()
    w.show()
    sys.exit(app.exec_())

enter image description here

© www.soinside.com 2019 - 2024. All rights reserved.