PySide2:如何重新实现 QFormLayout.takeRow()?

PySide2: How to re-implement QFormLayout.takeRow()?

我注意到 Pyside2 中的 QFormLayout 没有像 PyQt5 对应的 takeRow 方法。我试图子类化 QFormLayout 以合并类似的方法,但我已经 运行 进入运行时错误,因为 LabelRole 项目的删除行为不同于 FieldRole 物品。另一个问题是 LabelRole 项目实际上并没有从行中删除,即使行本身被删除。

以下是我一直在使用 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_())

我想了解为什么角色项的行为不同以及如何正确地重新实现该方法。

问题是标签是由 Qt 在内部根据字符串创建的,而不是通过在 Python 中显式创建 QLabel。这意味着当删除该行时,最后一个剩余的引用也将被删除,这将删除 C++ 端的标签。在那之后,Python 端剩下的就是一个空的 PyQt 包装器 - 因此当您尝试对其调用 setParent 时,将引发 RuntimeError,因为底层 C++ 部分不再存在。

因此,您的示例可以通过获取 python 对 label/field 对象的引用来修复 布局项被删除之前:

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