Search code examples
pythonpyqt5qlayout

Resize dock to height of visible contents


I'm creating a collapsible widget. It contains a table and is embedded in another widget beneath some group boxes. Everything is put into a dock. The table contained in the collapsible widget vertically fills the dock when the collapsible widget is expanded; the group boxes remain fixed. However, the dock resizes to the height of the group boxes and the collapsible widget button only if the dock hasn't been resized first.

Notice how after resizing the dock, the dock remains the same size as the table which was collapsed:

enter image description here

How can I have the dock resize like on first load, to the minimum height of the group boxes and toggle button? Or maybe a better question, how does the dock widget determine its minimum size and how can I advise it to be minimum size (if not through MinimumExpanding)?

import sys
from PyQt5 import QtCore, QtWidgets, QtWidgets


class CollapsibleWidget(QtWidgets.QWidget):

    def __init__(self, title="", parent=None):
        super().__init__(parent)

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

        self.content_layout = QtWidgets.QVBoxLayout()
        self.content_widget = QtWidgets.QWidget()
        self.content_widget.setLayout(self.content_layout)
        self.content_widget.hide()

        lay = QtWidgets.QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.addWidget(self.toggle_button, alignment=QtCore.Qt.AlignTop)
        lay.addWidget(self.content_widget)

    def on_pressed(self):
        checked = self.toggle_button.isChecked()
        self.toggle_button.setArrowType(QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
        self.content_widget.setVisible(checked)


class ControlWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        # Checkboxes
        self.checkbox1 = QtWidgets.QCheckBox("Checkbox1")
        self.checkbox2 = QtWidgets.QCheckBox("Checkbox2")

        # Buttons
        self.button1 = QtWidgets.QPushButton('Button1')
        self.button2 = QtWidgets.QPushButton('Button2')
        self.button3 = QtWidgets.QPushButton('Button3')

        # Checkbox group
        self.gb_checkbox = QtWidgets.QGroupBox("Checkboxes")
        self.layout_gb_checkbox = QtWidgets.QHBoxLayout()
        self.layout_gb_checkbox.addWidget(self.checkbox1)
        self.layout_gb_checkbox.addWidget(self.checkbox2)
        self.gb_checkbox.setLayout(self.layout_gb_checkbox)
        self.gb_checkbox.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)

        # Button group
        self.gb_button = QtWidgets.QGroupBox("Buttons")
        self.layout_gb_button = QtWidgets.QHBoxLayout()
        self.layout_gb_button.addWidget(self.button1)
        self.layout_gb_button.addWidget(self.button2)
        self.layout_gb_button.addWidget(self.button3)
        self.gb_button.setLayout(self.layout_gb_button)
        self.gb_button.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)

        # groups layout
        self.groups_layout = QtWidgets.QHBoxLayout()
        self.groups_layout.addWidget(self.gb_checkbox)
        self.groups_layout.addWidget(self.gb_button)

        # table
        self.table = QtWidgets.QTableWidget()
        for i in range(20):
            self.table.insertRow(i)

        # Collapsible widget
        self.collapsible_widget = CollapsibleWidget("Table")
        self.collapsible_widget.content_layout.addWidget(self.table)

        layout = QtWidgets.QVBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addLayout(self.groups_layout)
        layout.addWidget(self.collapsible_widget)

        self.setLayout(layout)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    controls = ControlWidget()

    # Dock
    dock_layout = QtWidgets.QVBoxLayout()
    dock_layout.setContentsMargins(4, 0, 4, 0)
    dock_layout.addWidget(controls)

    dock = QtWidgets.QDockWidget("Control Panel")
    dock_contents = QtWidgets.QWidget()
    dock_contents.setLayout(dock_layout)
    dock.setWidget(dock_contents)

    # central widget
    central_widget = QtWidgets.QWidget()
    central_widget.setStyleSheet('background-color: gray')

    # main window
    main_window = QtWidgets.QMainWindow()
    main_window.resize(640, 480)
    main_window.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock)
    main_window.setCentralWidget(central_widget)

    main_window.show()
    sys.exit(app.exec_())

I tried setting the sizeHint of the dock really low and setting sizePolicy on the dock to MinimumExpanding or Expanding. I expected the dock to then try resizing to the minimum, but then resize to the minimum of its contents. There was no noticeable change in behavior.

I tried accessing the dock within the on_pressed() call and forcing it to resize(). Again, no noticeable change in behavior.


Solution

  • Unfortunately, the layout of a QMainWindow (and the layout of the dock areas) is almost unaccessible, at least from python. The main problem is that dock widgets are added to an internal system of layouts that also keeps trace of manual resizing, and there's no way (at least, that I know of) to "reset" those sizes.

    There exist some possible workarounds, though.

    One idea is that the collapsible widget emits a signal whenever it's collapsed, and that signal is connected to a specialized function of the main window.

    In this case, I'll automatically connect the signal whenever a dock widget is set as parent of the main window (but there are other ways to do so). Then the trick is to check whether the dock is floating or not, and then, respectively:

    • resize it according to its minimum (vertical) size hint
    • force the vertical sizing of the dock
    class CollapsibleWidget(QtWidgets.QWidget):
        collapsed = QtCore.pyqtSignal()
        # ...
        def on_pressed(self):
            checked = self.toggle_button.isChecked()
            self.toggle_button.setArrowType(
                QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
            self.content_widget.setVisible(checked)
            if not checked:
                self.collapsed.emit()
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def childEvent(self, event):
            if event.added() and isinstance(event.child(), QtWidgets.QDockWidget):
                for resizable in event.child().findChildren(CollapsibleWidget):
                    resizable.collapsed.connect(self.collapsibleResized)
    
        def collapsibleResized(self):
            widget = self.sender()
            dock = widget.parent()
            while not isinstance(dock, QtWidgets.QDockWidget):
                dock = dock.parent()
            if dock.isFloating():
                def delayedResize():
                    dock.resize(dock.width(), dock.minimumSizeHint().height())
            else:
                def delayedResize():
                    self.resizeDocks(
                        [dock], 
                        [dock.widget().minimumSizeHint().height()], 
                        QtCore.Qt.Vertical
                    )
            QtWidgets.QApplication.processEvents()
            QtCore.QTimer.singleShot(0, delayedResize)
    

    There could be some issues whenever multiple dock are added (or tabified), and I'm not sure about restoring dock state, so you should probably do some deep testing.