Home > OS >  How to avoid over-packing non-srolling Qt layouts?
How to avoid over-packing non-srolling Qt layouts?

Time:07-04

A Qt packing layout, such as QVBoxLayout, can pack widgets inside it, such as buttons. In this case, they will be packed vertically as shown in image below:

enter image description here

When we pack too many widgets inside such a layout, and since scrolling is not added by default, the buttons will eventually get squeezed onto each other up to a point that they will overlap, as shown below:

enter image description here

My questions are:

  1. How to tell Qt to not show/pack widgets beyond the available viewing space in the non-scrolling layout?
  2. How to handle the case when the window is resized? I.e. Qt should add/remove widgets accordingly. E.g. if there is extra space available, then perhaps Qt should add some extra widgets that it couldn't add previously.

To be specific: "too many packed widgets" is when the widgets start invading spaces of other widgets, including their inter-widget spacings or margins.


Appendix

Images above are generated by this code below as run in a tile in i3, which is a modified version of this.

from PyQt5 import QtCore, QtWidgets

app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
for i in range(40):
    layout.addWidget(QtWidgets.QPushButton(str(i   1)))
widget.show()
app.exec_()

When too many widgets are packed:

  • If the window is tiled, you see them overcrowded as in in the image.
  • If the window is floating, the window will keep growing until it is no longer fully visible in the monitor.

None of these outcomes are acceptable in my case. My goal is to have Qt only pack as much as will be visible, and add/remove/hide/show dynamically as the window gets resized.

CodePudding user response:

Try this code. It does not rely on QVBoxLayout but it basically does the same as this layout. It hides the child widgets which are outside of the area. There are no partially visible widgets.

from PyQt5 import QtWidgets

class Container(QtWidgets.QWidget):
    _spacing = 5

    def __init__(self, parent=None):
        super().__init__(parent)
        y = self._spacing
        for i in range(40):
            button = QtWidgets.QPushButton("Button"   str(i   1), self)
            button.move(self._spacing, y)
            y  = button.sizeHint().height()   self._spacing

    def resizeEvent(self, event):
        super().resizeEvent(event)
        for child in self.children():
           if isinstance(child, QtWidgets.QWidget):
               child.resize(self.width() - 2 * self._spacing, child.height())
               child.setVisible(child.geometry().bottom() < self.height())


app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()

Note that is in fact does not add nor remove widgets dynamically, this would be much more code and it would probably be very depending on your specific use case. Moreover it feels as a premature optimization. Unless you really need it, do not do it.

UPDATE: I experimented with the code above and proposed some improvements. I especially wanted to make it responsive to changes in child widgets. The problem is that if the child widget changes it size, the parent container must be re-layouted. The code above does not react in any way. To make it responsive, we need to react to LayoutRequest event. Note that in the code below, I have created three types of buttons - one add a line to itself, other increases font size, and yet another decreases font size.

from PyQt5 import QtCore, QtWidgets

def changeFontSize(increment):
    font = QtWidgets.QApplication.font()
    font.setPointSize(font.pointSize()   increment)
    QtWidgets.QApplication.setFont(font)

class Container(QtWidgets.QWidget):
    _spacing = 5
    _children = []  # maintains the order of creation unlike children()

    def __init__(self, parent=None):
        super().__init__(parent)
        for i in range(100):
            child = QtWidgets.QPushButton(self)
            child.installEventFilter(self)
            # these are just to test various changes in child widget itself to force relayout
            r = i % 3
            if r == 0:
                text = "New line"
                onClicked = lambda state, w=child: w.setText(w.text()   "\nclicked")
            elif r == 1:
                text = "Bigger font"
                onClicked = lambda: changeFontSize(1)
            elif r == 2:
                text = "Smaller font"
                onClicked = lambda: changeFontSize(-1)

            child.setText(text)
            child.clicked.connect(onClicked)
            self._children.append(child)

    def resizeEvent(self, event):
        super().resizeEvent(event)    
        self._relayout() 
           
    def event(self, event):
        if event.type() == QtCore.QEvent.LayoutRequest:
            self._relayout()
            
        return super().event(event)

    def _relayout(self):
        y = self._spacing
        for child in self._children:
            h = child.sizeHint().height()
            child.move(self._spacing, y)
            child.resize(self.width() - 2 * self._spacing, h)
            y  = h   self._spacing
            child.setVisible(y < self.height()) 

app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()

This code is satisfactory, however it is not perfect. I have observed that when the container is being re-layouted and some of the child widgets will change its visibility state, re-layouting is called again. This is not needed but I have not discovered how to prevent it.

Maybe there is some better way...

  • Related