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_())