0

As part of my PyQt5 application I have a side control panel with numerous sections (i.e. Load buttons, data manipulation, data point comparison etc.)

In order to clean up the look, and not have one really long scrollable area, I am trying to place each section into its own collapsible box. I have implemented a collapsible box from the post How to create collapsible box in PyQt and it is working well (thanks to @eyllanesc for the nice implementation).

As part of my implementation I have a FigureCanvas widget (from from matplotlib.backends.backend_qt5agg) within one of these boxes where I will be plotting data dependent on selections from the user. My issue is that I am getting a ValueError on load due to the fact that the figure size is not finite (the boxes are collapsed by default and so the space for the figure is not available). ValueError: figure size must be positive finite not [ 1.76 -0.05]

I understand the base of the issue but wondering if there is some way around it.

I tried not creating the FigureCanvas unless the box was expanded but it seems the heights of the boxes are set on start-up and unless I create a blank figure initially, the box does not show the figure correctly when required. This is is also present if I have, for example, a list of radio buttons and try to increase the number of buttons based on user selections (the size of the box does not change and the buttons are all squashed)

Below is a MWE based off the code in How to create collapsible box in PyQt with the amended section at the bottom shown between ##

from PyQt5 import QtCore, QtGui, QtWidgets

from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvas


class CollapsibleBox(QtWidgets.QWidget):
    def __init__(self, title="", parent=None):
        super(CollapsibleBox, self).__init__(parent)

        self.toggle_button = QtWidgets.QToolButton(
            text=title, checkable=True, checked=False
        )
        self.toggle_button.setStyleSheet("QToolButton { border: none; }")
        self.toggle_button.setToolButtonStyle(
            QtCore.Qt.ToolButtonTextBesideIcon
        )
        self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
        self.toggle_button.pressed.connect(self.on_pressed)

        self.toggle_animation = QtCore.QParallelAnimationGroup(self)

        self.content_area = QtWidgets.QScrollArea(
            maximumHeight=0, minimumHeight=0
        )
        self.content_area.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
        )
        self.content_area.setFrameShape(QtWidgets.QFrame.NoFrame)

        lay = QtWidgets.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(
            QtCore.QPropertyAnimation(self, b"minimumHeight")
        )
        self.toggle_animation.addAnimation(
            QtCore.QPropertyAnimation(self, b"maximumHeight")
        )
        self.toggle_animation.addAnimation(
            QtCore.QPropertyAnimation(self.content_area, b"maximumHeight")
        )

    @QtCore.pyqtSlot()
    def on_pressed(self):
        checked = self.toggle_button.isChecked()
        self.toggle_button.setArrowType(
            QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
        )
        self.toggle_animation.setDirection(
            QtCore.QAbstractAnimation.Forward
            if not checked
            else QtCore.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
    import random

    app = QtWidgets.QApplication(sys.argv)

    w = QtWidgets.QMainWindow()
    w.setCentralWidget(QtWidgets.QWidget())
    dock = QtWidgets.QDockWidget("Collapsible Demo")
    w.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
    scroll = QtWidgets.QScrollArea()
    dock.setWidget(scroll)
    content = QtWidgets.QWidget()
    scroll.setWidget(content)
    scroll.setWidgetResizable(True)
    vlay = QtWidgets.QVBoxLayout(content)
    for i in range(2):
        box = CollapsibleBox("Collapsible Box Header-{}".format(i))
        vlay.addWidget(box)
        lay = QtWidgets.QVBoxLayout()
        for j in range(8):
            label = QtWidgets.QLabel("{}".format(j))
            color = QtGui.QColor(*[random.randint(0, 255) for _ in range(3)])
            label.setStyleSheet(
                "background-color: {}; color : white;".format(color.name())
            )
            label.setAlignment(QtCore.Qt.AlignCenter)
            lay.addWidget(label)

        box.setContentLayout(lay)

    ##########################
    # Altered from https://stackoverflow.com/questions/52615115/how-to-create-collapsible-box-in-pyqt
    ##########################

    # ------------------------
    # Works if not in a box
    # ------------------------

    canvas = FigureCanvas(Figure())
    vlay.addWidget(canvas)
    ax = canvas.figure.subplots()

    # ------------------------
    # Error if placed in a box
    # ------------------------

    box = CollapsibleBox("Collapsible Box Header-Figure")
    vlay.addWidget(box)
    lay = QtWidgets.QVBoxLayout()
    box.setContentLayout(lay)

    canvas = FigureCanvas(Figure())
    lay.addWidget(canvas)
    ax = canvas.figure.subplots()

    ###############

    vlay.addStretch()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())
  • You could try setting the minimum height of your canvas to something positive. – Heike Jun 15 '21 at 11:29
  • Thanks @Heike adding `canvas.setMinimumHeight(1)` does indeed stop the error but when you expand the box, the figure does not expand (or shown fully). It also does not seem to matter what value I set that to (1, 10, 100, 1000). – Kevin Sweeney Jun 16 '21 at 11:17
  • In `CollapsibleBox.setContentLayout`, the minimum and maximum height of the box is calculated from the current sizeHint layout. You could try adding all the widgets to `lay` before calling `box.setContentLayout(lay)`. – Heike Jun 16 '21 at 13:27

0 Answers0