Search code examples
pythonpyqt6

How to prevent QListWidget from shrinking when showing other widget?


I want a checkbox to toggle visibility of the label/button group below it. Right now the listwidget shrinks, buttons below it move up and checkbox moves up. What I want is the opposite: the listitem doesn't change size, the del/clear buttons don't move, and instead the big button at the bottom is moved down.

I've tried setMinimumHeight() or setFixedHeight() on the listwidget with various values. Also tried putting the listwidget and delete/clear buttons in a layout and setting that height. Tried to set Qt.AlignmentFlag to Top/bottom on listitem, checkbox, group, buttons etc. Noticed no change. Found SetColumnStretch and SetRowStretch, but couldn't get that to work either. It either moves less upwards, also moves downwards, or the delete/clear buttons move on top of the listwidget.

So, could someone please point me in the right direction to get the "Visible big button" in this example to move up/down as the group above it is hidden/shown, and not the listwidget and del/clear buttons? I want the layout to grow when window is resized (like it does now, that is), so I don't think I can use "Fixed" values really. Here's a gif describing my problem:

example gif

This is my test code:

from PyQt6.QtWidgets import (
    QApplication,
    QGridLayout,
    QHBoxLayout,
    QListWidget,
    QPushButton,
    QCheckBox,
    QWidget,
    QLabel
)


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

        listwidget = QListWidget()
        del_btn = QPushButton(text="Delete item")
        clear_btn = QPushButton(text="Clear")
        checkbox = QCheckBox(text="Visibility toggle checkbox")
        hidden_label = QLabel(text="Hidden label")
        hidden_btn = QPushButton(text="Hidden button")
        visible_btn = QPushButton(text="Visible big button")

        group_layout = QHBoxLayout()
        group_layout.addWidget(hidden_label)
        group_layout.addWidget(hidden_btn)
        self.hidden_widget = QWidget()
        self.hidden_widget.setLayout(group_layout)

        main_layout = QGridLayout(self)
        main_layout.addWidget(listwidget, 0, 0, 1, 2)
        main_layout.addWidget(del_btn, 1, 0)
        main_layout.addWidget(clear_btn, 1, 1)
        main_layout.addWidget(checkbox, 2, 0)
        main_layout.addWidget(self.hidden_widget, 3, 0, 1, 2)
        main_layout.addWidget(visible_btn, 4, 0, 1, 2)

        listwidget.addItem("When clicking the checkbox toggle")
        listwidget.addItem("I want the hidden label\nand button to be visible")
        listwidget.addItem("But without the listbox height shrinking")
        listwidget.addItem("and the checkbox moving upward.")
        listwidget.addItem("The visible big button should\ninstead be moved downward.")
        listwidget.addItem("Possible?")

        checkbox.setChecked(True)

        self.hidden_widget.hide()

        visible_btn.setFixedHeight(50)
        visible_btn.setStyleSheet("font-size: 14pt;")

        checkbox.stateChanged.connect(self.checkbox_toggled)

    def checkbox_toggled(self):
        self.hidden_widget.setHidden(not self.hidden_widget.isHidden())


if __name__ == "__main__":
    app = QApplication([])
    window = Widget()
    window.show()
    app.exec()

Edit: As musicamante points out in the comments, what I really want is to increase the window size. I found this thread and I changed my checkbox_toggled to this:

def checkbox_toggled(self):
        state = not self.hidden_widget.isVisible()
        if state:  # toggle to visible = expand window
            self.hidden_widget.show()
            self.resize(self.width(), self.height() + 48)
        else:  # toggle to hidden = shrink window
            self.hidden_widget.hide()
            self.resize(self.width(), self.height() - 48)

and it works for me since I'm the only one who is going to use this so I know what not to do. The 48 can probably be set dynamically with something like self.hidden_widget.sizeHint.height() or similar. Off to experiment. :)

Thanks alot!


Solution

  • In order to achieve this, you have to adjust the height of the window so that it has enough space for the widget that is going to be shown, otherwise the layout will always try to use the current available space, possibly shrinking widgets that allow it: in your case, the list widget, which has a sizeHint() larger than its minimum allowed size (the minimumSizeHint()); all the other widgets have a Fixed vertical size policy, so if you didn't have the list widget, the window would have been enlarged automatically.

    What you can do is to compute the space that would be required by the button and the layout spacing (since another item is being shown), and set the new height of the current geometry, then show the widget and set the new geometry.

    Note that due to the complex way layout management works (especially for complex widgets like scroll areas), setGeometry() might need to be called twice, after processing currently pending events.

    Finally, when you want to hide the widget back, follow the same procedure but subtracting the difference from the current height.

        def checkbox_toggled(self):
            if self.isMaximized():
                return
    
            height = self.hidden_widget.sizeHint().height() + self.layout().verticalSpacing()
            if self.hidden_widget.isVisible():
                geo = self.geometry()
                geo.setHeight(geo.height() - height)
                self.hidden_widget.hide()
                self.setGeometry(geo)
                # this is necessary as the size hints may not be fully updated yet
                QApplication.processEvents()
                if self.geometry() != geo:
                    self.setGeometry(geo)
            else:
                geo = self.geometry()
                geo.setHeight(geo.height() + height)
                self.setGeometry(geo)
                self.hidden_widget.show()
    

    I recommend to do some research about the primary elements used in layout managements:

    • sizeHint() returns the preferred size of a widget; for container widgets (having their own layouts), it normally returns the sizeHint() of their layout, computed using the sizeHint() of all items it manages;
    • minimumSizeHint() is the minimum possible size, unless setMinimum...() functions are called with dimensions larger than 1; similarly to sizeHint(), by default it returns the minimumSize() of the layout of a container widget;
    • sizePolicy() is queried by the layout managing the geometry of a child widget, so that it knows if and how that widget can be resized when the parent widget is smaller or bigger than the overall size hint;
    • QSizePolicy the object returned by sizePolicy() calls, instructing the layout about the resizing capabilities of each of its managed items;

    Note that the stateChanged signal of QCheckBox should normally be used only when you're actually interested in the Qt.CheckState, specifically for 3-state check boxes; if you only need to trigger a function based on the on/off check state, just use the standard toggled signal.