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:
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)