Search code examples
pythonpyqt5positionqt5python-3.8

Why is an absolute positioned widget not shown?


Using Qt5 I am trying to make a widget work using absolute positioning. The code below is a minimum working example of something I am trying to do. A quick walk through of the code:

  • CentralWidget is the central widget of the main window and holds MyWidget using absolute positioning, e.g. without using any layouts.
  • MyWidget does not set its child widgets immediately but provides a SetData method which first removes all current child widgets and then adds the new child widgets to its layout.
  • SetData is triggered using a timer in the main window.

I commented out two lines of code. The first "enables" relative positioning using layouts by adding a layout to CentralWidget. This line shows what I am trying to achieve but with absolute positioning. The second comment enables some debug information:

MyWidget
  layout.count: 3
  size: PyQt5.QtCore.QSize(-1, -1)
  sizeHint: PyQt5.QtCore.QSize(200, 100)

CentralWidget
  size: PyQt5.QtCore.QSize(18, 18)
  sizeHint: PyQt5.QtCore.QSize(18, 18)

What I am doing wrong in order for MyWidget to be visible using absolute positioning?

Code:

from PyQt5 import QtCore, QtWidgets
import sys


class MyWidget(QtWidgets.QWidget):
    z = 0

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

        self._layout = QtWidgets.QVBoxLayout(self)

    def SetData(self):
        while self._layout.count() > 0:
            widget = self._layout.takeAt(0).widget()
            widget.hide()
            widget.deleteLater()

        for i in range(3):
            self._layout.addWidget(QtWidgets.QLabel(str(MyWidget.z * 10 + i)))

        MyWidget.z += 1


class CentralWidget(QtWidgets.QWidget):
    def __init__(self, parent):
        super().__init__(parent)

        self._myWidget = MyWidget(self)

        # QtWidgets.QHBoxLayout(self).addWidget(self._myWidget)

    def SetData(self):
        self._myWidget.SetData()
        # print("MyWidget\n  layout.count: {}\n  size: {}\n  sizeHint: {}\n\nCentralWidget\n  size: {}\n  sizeHint: {}\n\n".format(self._myWidget.layout().count(), self.sizeHint(), self.size(), self._myWidget.sizeHint(), self._myWidget.size()))


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        centralWidget = CentralWidget(self)
        self.setCentralWidget(centralWidget)

        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect(centralWidget.SetData)
        self._timer.start(500)


def main():
    app = QtWidgets.QApplication(sys.argv)

    mainWindow = MainWindow()
    mainWindow.show()

    app.exec_()


if __name__ == "__main__":
    main()


Solution

  • The reason for this behavior is directly related to the fact that the widget is not added to a layout and its contents are added after being shown.

    In fact, if you call centralWidget.SetData() upon initialization and before mainWindow.show(), it will work as expected.

    A lot of things happen when you add a child widget to a layout, and this usually involves multiple calls to the children size hints, allowing the parent to adapt its own size hint, and, after that, adapt its size and that of its children.

    If that "container widget" is itself contained in another layout, that widget will be automatically resized (based on its hint) in the next cycle of events, but this doesn't happen in your case, since yours is a "free" widget.

    The function you are looking for is QWidget.adjustSize(), but, for the aforementioned reasons, you cannot call it immediately after adding the children widgets.
    To overcome your issue, you can call QApplication.processEvents() before adjustSize(), or, eventually, use a 0-based single shot QTimer:

        def SetData(self):
            while self._layout.count() > 0:
                widget = self._layout.takeAt(0).widget()
                widget.hide()
                widget.deleteLater()
    
            for i in range(3):
                self._layout.addWidget(QtWidgets.QLabel(str(MyWidget.z * 10 + i)))
    
            MyWidget.z += 1
    
            QtCore.QTimer.singleShot(0, self.adjustSize)