1

I've noticed that QFormLayout in Pyside2 does not have the takeRow method like its PyQt5 counterpart. I've attempted to subclass QFormLayout to incorporate a similar method, but I've run into Runtime Errors, as the removal behavor of the LabelRole item is different than the FieldRole item. Another issue being that the LabelRole item does not actually get taken off the row even when the row itself is removed.

The following is the test sample I've been working with using Python 3.8.6:

from PySide2.QtWidgets import *
import sys


class MyFormLayout(QFormLayout):
    def __init__(self, *args, **kwargs):
        super(MyFormLayout, self).__init__(*args, **kwargs)
        self.cache = []
        print(f"Formlayout's identity: {self=}\nwith parent {self.parent()=}")

    def takeRow(self, row: int):
        print(f"Called {self.takeRow.__name__}")
        print(f"{self.rowCount()=}")
        label_item = self.itemAt(row, QFormLayout.LabelRole)
        field_item = self.itemAt(row, QFormLayout.FieldRole)
        print(f"{label_item=}\n{field_item=}")

        self.removeItem(label_item)
        self.removeItem(field_item)
        self.removeRow(row)  ## <-- This seems necessary to make the rowCount() decrement. Alternative?
        label_item.widget().setParent(None)  ## <-- Runtime Error Here?
        field_item.layout().setParent(None)

        self.cache.append(label_item.widget(), field_item)
        print(f"{self.rowCount()=}")
        print(f"{self.cache=}")
        print(self.cache[0])
        print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")
        return label_item, field_item

    def restoreRow(self, insert_idx: int):
        print(f"Called {self.restoreRow.__name__}")
        print(f"{self.rowCount()=}")
        print(f"{self.cache=}")
        to_insert = self.cache.pop()
        self.insertRow(insert_idx, to_insert[0], to_insert[1])
        print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")


class MyWindow(QWidget):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.mainlay = MyFormLayout(self)
        self.cmb = QComboBox()
        self.cmb.addItems(["Placeholder", "Remove 1 and 2"])
        self.cmb.currentTextChanged.connect(self.remove_rows_via_combo)
        self.current_text = self.cmb.currentText()
        self.hlay1, self.le1, self.btn1 = self.le_and_btn(placeholderText="1")
        self.hlay2, self.le2, self.btn2 = self.le_and_btn(placeholderText="2")
        self.hlay3, self.le3, self.btn3 = self.le_and_btn(placeholderText="3")
        self.hlay4, self.le4, self.btn4 = self.le_and_btn(placeholderText="4")
        self.remove_btn = QPushButton("Remove", clicked=self.remove_row_via_click)
        self.restore_btn = QPushButton("Restore", clicked=self.restore_a_row_via_click)
        self.mainlay.addRow("Combobox", self.cmb)
        for ii, hlayout in zip(range(1, 5), [self.hlay1, self.hlay2, self.hlay3, self.hlay4]):
            self.mainlay.addRow(f"Row {ii}", hlayout)
        self.mainlay.addRow(self.remove_btn)
        self.mainlay.addRow(self.restore_btn)

    @staticmethod
    def le_and_btn(**kwargs):
        hlay, le, btn = QHBoxLayout(), QLineEdit(**kwargs), QPushButton()
        hlay.addWidget(le)
        hlay.addWidget(btn)
        return hlay, le, btn

    def remove_row_via_click(self):
        self.mainlay.takeRow(1)

    def restore_a_row_via_click(self):
        self.mainlay.restoreRow(1)

    def remove_rows_via_combo(self, text):
        print(f"{self.remove_rows_via_combo.__name__} received the text: {text}")
        if text == "Remove 1 and 2":
            self.mainlay.takeRow(1)
            self.mainlay.takeRow(1)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = MyWindow()
    win.show()
    sys.exit(app.exec_())

I would like to understand why the behavior of the role items is different and how the method may be properly re-implemented.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336

1 Answers1

1

The problem is that the label was created internally by Qt from a string, rather than by explicitly creating a QLabel in Python. This means that when the row is removed, the last remaining reference is also removed, which deletes the label on the C++ side. After that, all that's left on the Python side is an empty PyQt wrapper - so when you try to call setParent on it, a RuntimeError will be raised, because the underlying C++ part no longer exists.

Your example can therefore be fixed by getting python references to the label/field objects before the layout-item is removed:

class MyFormLayout(QFormLayout):
    ...    
    def takeRow(self, row: int):
        print(f"Called {self.takeRow.__name__}")
        print(f"{self.rowCount()=}")
        label_item = self.itemAt(row, QFormLayout.LabelRole)
        field_item = self.itemAt(row, QFormLayout.FieldRole)
        print(f"{label_item=}\n{field_item=}")
        
        # get refs before removal
        label = label_item.widget()
        field = field_item.layout() or field_item.widget()

        self.removeItem(label_item)
        self.removeItem(field_item)
        self.removeRow(row)

        label.setParent(None)
        field.setParent(None)

        self.cache.append((label, field))

        print(f"{self.rowCount()=}")
        print(f"{self.cache=}")
        print(self.cache[0])
        print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&")

        return label, field
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Understood. Following this explanation and through testing, it was also clear that if `MyFormLayout` is part of some larger nested structure (i.e. it is within a widget of a QMainWindow), all widgets within a layout type of FieldRole would become owned by those "higher order" widgets. In such an event, I observed that it was necessary to have all widgets within this FieldRole layout also have `setParent(None)`. Otherwise, they would remain visible even when the FieldRole layout had its parent set to None. This is apparently due to the behavior of parent-child ownership. – Maurice Pasternak Feb 25 '21 at 05:27