John Kendrick
John Kendrick

Reputation: 1

Warning message on removing a row in QFormLayout

I have a QFormLayout with several rows in it. Including one row with another QFormLayout. I want to delete a row and insert another row. Although I can get it to work there is a message every time I delete a row:

QFormLayout::takeAt: Invalid index 0

I'm not sure if I am doing something wrong by embedding a form inside another, but there is clearly an issue

An example of the code that shows this behaviour is shown below:

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWidgets import QFormLayout, QLabel, QPushButton
from PyQt5.QtGui import QPalette, QColor

class MainWindow(QMainWindow):

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

        self.setWindowTitle("My App")
        layout = QFormLayout()
        layout.addRow(QLabel('red'),Color('red'))
        layout.addRow(QLabel('green'),Color('green'))
        insideLayout = QFormLayout()
        insideLayout.addRow(QLabel('label 1'),QPushButton('Label 1'))
        insideLayout.addRow(QLabel('label 2'),QPushButton('Label 2'))
        layout.addRow(QLabel('inside'),insideLayout)
        row = layout.rowCount()-1
        layout.addRow(QLabel('blue'),Color('blue'))
        layout.removeRow(row)

        widget = QWidget()
        widget.setLayout(layout)

I was expecting to be able to remove a row in the QFormLayout fairly straightforwardly. I haven't tried any other approaches.

Upvotes: 0

Views: 183

Answers (1)

musicamante
musicamante

Reputation: 48260

tl;dr

That is a warning caused by internal calls of QFormLayout (specifically, the "parent" one you are using).

The expected behavior is still achieved, as it doesn't affect the result, so you can safely ignore it.

Explanation

An important thing to be aware of is that layout managers in Qt use the base QLayoutItem class, which is an abstract object that represents an item within a layout: that could be a widget, a spacer, or even a further nested layout. This is done to have a transparent interface, so that the layout can access the items size hints/policies/restraints and set their geometries no matter of (or considering) the item type.

Interestingly, a QLayout (which is the basis for all Qt layouts) also inherits from QLayoutItem, and there is no real restriction about what it could represent: it could be empty (no widget, no spacer and no layout) or, theoretically, represent a combination of them (unlikely, but not strictly forbidden).

Now, removeRow() uses a private clearAndDestroyQLayoutItem() static function, with a while loop that calls takeAt for items that represent a layout so that it clears all the contents of that item, eventually calling itself recursively whenever it represents a nested layout.

This is what happens when removeRow() is called:

  • get the pair of QLayoutItems (label and field) for the given row;
  • call clearAndDestroyQLayoutItem() with both of them, which will do the following:
  • if it is a widget, delete it;
  • if it is a layout, then do a while that loops while calls to takeAt(0) return a valid QLayoutItem;
    • call clearAndDestroyQLayoutItem() with that result;

This can be approximately translated into Python like this:

def clearAndDestroyQLayoutItem(item):
    if item:
        widget = item.widget()
        if widget:
            widget.deleteLater()
        layout = item.layout()
        if layout is not None:
            while child := layout.takeAt(0) is not None:
                 clearAndDestroyQLayoutItem(child)
            layout.deleteLater() # extra safety, not in the original C++ code


class QFormLayout(QLayout):
    ...
    def removeRow(self, row):
        res = self.takeRow(row)
        clearAndDestroyQLayoutItem(result.labelItem)
        clearAndDestroyQLayoutItem(result.fieldItem)

Notes: 1. the C++ code actually uses delete for the widget, but Python that doesn't work in the same way (and del isn't acceptable), so deleteLater is safer; an alternative may be to use sip.delete(), but that should be used with extreme care (it would probably crash in your case); 2. the if layout is not None: is required because the __bool__ operator of QLayout is overridden in PyQt, returning False if the layout has no items even if a layout object exists; 3. the while loop above uses the assignment operator introduced in Python 3.8.

In any case, what's important is that takeAt(0) is always called until it returns nothing (the layout is empty). The default implementation requires it to return nullptr in C++ (None in Python) if no layout item exists at the given index, and QFormLayout follows that correctly.

The only difference with other layout managers is the warning QFormLayout produces, which was probably put there for "lazy preventive debugging", but it doesn't change its behavior and shouldn't create any issue, unless the destroyed objects in that row are implemented in some weird or extremely complex ways (which would probably present other issues anyways).

As said above, you can safely ignore that warning in normal cases like the one you provided.

Upvotes: 1

Related Questions