Skip to content
Advertisement

How to properly submit data when using QDataWidgetMapper?

I’m using a custom model subclassed from QAbstractTableModel, my data is a list of dataclasses.

I’ve set up a simple GUI with a QListView and two QLineEdits, like so:

Simple GUI with a listview and two line-edits

import sys
import dataclasses
from typing import List, Any
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *


@dataclasses.dataclass()
class StorageItem:

    field1: str = 'Item °1'
    field2: int = 42


class StorageModel(QAbstractTableModel):

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

        self._data: List[StorageItem] = [StorageItem()]

    def data(self, index: QModelIndex, role: int = ...) -> Any:
        if not index.isValid():
            return

        item = self._data[index.row()]
        col = index.column()

        if role in {Qt.DisplayRole, Qt.EditRole}:
            if col == 0:
                return item.field1
            elif col == 1:
                return item.field2
            else:
                return None

    def setData(self, index: QModelIndex, value, role: int = ...) -> bool:
        print('dataChanged')

        if not index.isValid() or role != Qt.EditRole:
            return False

        item = self._data[index.row()]
        col = index.column()

        if col == 0:
            item.field1 = value
        elif col == 1:
            item.field2 = value

        self.dataChanged.emit(index, index)
        return True

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        return Qt.ItemFlags(
            Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
        )

    def rowCount(self, parent=None) -> int:
        return len(self._data)

    def columnCount(self, parent=None) -> int:
        return len(dataclasses.fields(StorageItem))


class MainWindow(QMainWindow):

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

        cent_widget = QWidget()
        self.setCentralWidget(cent_widget)

        # Vertical Layout
        v_layout = QVBoxLayout()
        v_layout.setContentsMargins(10, 10, 10, 10)

        self.model = StorageModel()

        # Listview
        self.listview = QListView()
        self.listview.setModel(self.model)
        v_layout.addWidget(self.listview)

        # Horizontal Layout
        h_layout = QHBoxLayout()
        h_layout.setContentsMargins(*[0]*4)

        self.field1 = QLineEdit()
        h_layout.addWidget(self.field1)

        self.field2 = QLineEdit()
        h_layout.addWidget(self.field2)

        v_layout.addLayout(h_layout)
        cent_widget.setLayout(v_layout)

        # Set Mapping
        mapper = QDataWidgetMapper()
        mapper.setSubmitPolicy(QDataWidgetMapper.AutoSubmit)
        mapper.setModel(self.model)
        mapper.addMapping(self.field1, 0)
        mapper.addMapping(self.field2, 1)
        mapper.toFirst()

        # self.field1.textChanged.connect(lambda: mapper.submit())


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()


if __name__ == '__main__':
    main()

I’m trying to achieve that whenever I change the contents of the first QLineEdit, the list-view is updated as well.
From reading the documentation for QDataWidgetMapper I know that the model should be updated whenever the current widget loses focus, but it isn’t. No matter what I enter in the edit-fields, the model’s setData-method is never called.
Even if I edit the item in the list-view, the line-edit’s contents don’t change.

I discovered that when I connect the text-field’s textChanged-signal to the mapper’s submit-method, everything works, but the dataChanged-method is called three times, and I don’t understand why.
Even stranger, now the text-field’s contents are updated whenever I edit the item in the list-view, although connecting to the textChanged signal is (at least I think so) only a one-way connection.

What am I doing wrong? I’m clearly missing something, as QDataWidgetMapper‘s SubmitPolicy would be completely useless if this was the right way to do it.

Advertisement

Answer

Your mapper gets deleted as soon as __init__ returns, because there’s no persistent reference for it. This is a common mistake for Qt objects in PyQt, usually caused by the fact that widgets added to a parent or layout become persistent even if there’s no python reference, but the fact is that adding a widget to a layout actually creates a persistent reference (the parent widget takes “ownership”, in Qt terms), thus preventing garbage collection.

Just make it an instance member or add the parent argument:

    self.mapper = QDataWidgetMapper()
    # alternatively (which is "safer" from the Qt point of view):
    mapper = QDataWidgetMapper(self)
    # but since you will probably need further access in any case:
    self.mapper = QDataWidgetMapper(self)
User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement