0

guys.

Asking for your help to troubleshoot my test script. I am practicing to make collapsible button with widgets inside.

Script was mainly taken from another question in stackoverflow about collapsible buttons.

So I am trying to put under QTabWidget my class CollpsibleBox(QWidget). Problem is that my CollapsibleBox is acting very weird - buttons are jumping , sometimes it doesn't open/close properly.

I was wondering if it's some mistake in placing correctly my widget under QTabWidget or is there some problem with animation?

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']}
tabList = ['Main', 'Extra']
_ui = dict()

class MainWindow(QDialog):
    def __init__(self, parent=None):
        QDialog.__init__(self, parent=parent)
        self.create()

    def create(self, **kwargs):
        _ui['mainLayout'] = QVBoxLayout()
        _ui['tabWidget'] = QTabWidget()
        _ui['mainLayout'].addWidget(_ui['tabWidget'])
        for tab in tabList:
            _ui['tab' + tab] = QWidget()
            _ui['tabWidget'].addTab(_ui['tab' + tab], tab)

        _ui['tabExtra'].layout = QVBoxLayout()
        _ui['tabExtra'].setLayout(_ui['tabExtra'].layout)

        _ui['content'] = QWidget()
        _ui['tabExtra'].layout.addWidget(_ui['content'])

        vlay = QVBoxLayout(_ui['content'])
        for name in extraDict.keys():
            box = CollapsibleBox(name)
            vlay.addWidget(box)
            lay = 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)
                lay.addWidget(label)

            box.setContentLayout(lay)
        self.setLayout(_ui['mainLayout'])

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_())
Vlad
  • 387
  • 3
  • 17

1 Answers1

2

The problem is that you are only adding two widgets to the full layout, and the layout will try to place them as better as possible (tipically at the center of the area that is available for each widget, based on its size hints).

You could either set the alignment of the widget for the layout (placing the buttons on top of their available space):

        vlay = QVBoxLayout(_ui['content'])
        for name in extraDict.keys():
            box = CollapsibleBox(name)
            vlay.addWidget(box, alignment=Qt.AlignTop)

Or add a stretch to the bottom of the layout:

        vlay = QVBoxLayout(_ui['content'])
        for name in extraDict.keys():
            # ...
        vlay.addStretch(1)

which will position all buttons on top of the layout.

As a side note, I'd suggest you to avoid the dictionary logic for the ui, as it might become very confusing and prone to errors. If you really need to do that for some (I hope, very good) reason that's ok, but please avoid it when asking questions: it makes really hard to read your code, and people might end up just ignoring your question at all.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you for your help, it really helped. Also thanks for note about dictionary, I am an amateur in coding and still don't know how to make proper codes. Can you please suggest me then how to avoid dictionary? I am making dictionaries , because sometimes in my scripts I find that I need to use some information from mainWindow class in other classes without making global variables. – Vlad Dec 02 '19 at 19:08
  • 1
    You're welcome! You don't need to use a dict for this, just create class instance attributes (eg. `self.tabWidget = QTabWidget()`), which is exactly what Qt does when using `setupUi` or `uic.loadUi` with .ui files created from Designer. You only have to ensure that attribute names are unique (but that would be the same for dictionaries) and don't overwrite the basic QWidget/QObject methods. Since you're creating the ui from code, you can avoid creating persistent instance attributes for objects you don't need tracking for (usually widget layouts, as you can access them with `widget.layout()`) – musicamante Dec 02 '19 at 20:21
  • Ohh... I see, true that. Then can I ask one last question about this topic. How do you handle 'for loop' then ? If you need to make a lot of same buttons, but with unique naming , is there any other option then dictionary ? – Vlad Dec 03 '19 at 06:38
  • 1
    Usually one would use a list to group those buttons (remember that there is QButtonGroup), or eventually by means of setattr (eg. `setattr(self, 'button_{:02}'.format(b), QPushButton(str(b)))`). Look around S.O. for answers about creating dynamic attribute names for classes. – musicamante Dec 03 '19 at 14:07