PDA

View Full Version : Delegate does not invoke setModelData() method



tristam
8th March 2014, 04:33
I have an application in which I read and write to disk using data from a custom tree model. The model data is mapped to widgets on a standard form UI using QDataWidgetMapper. I noticed that the data reads in perfectly--except for QComboBox widgets. (The model holds the data just fine--it's the widget that doesn't properly display it.) After doing some searching, it seems that these require a custom delegate in order for the editor data to be correctly set from the model. I decided to test this out by developing a very simple form backed by a QAbstractTableModel. I have mostly followed this tutorial: http://doc.qt.digia.com/qq/qq21-datawidgetmapper.html ; my code (Python 2.7.3/PySide 1.2.1) is below:



#!/usr/bin/python
from PySide.QtCore import *
from PySide.QtGui import *
import sys

# Since the delegate only needs to communicate information between the model and the widget mapper,
# we only need to provide implementations of the setEditorData() and setModelData() functions
class CustomDelegate(QAbstractItemDelegate):

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

def setEditorData(self, editor, index):
if editor.property("currentIndex") is not None:
value = index.data(Qt.DisplayRole)
editor.setProperty("currentIndex", index.data())
return
text = index.model().data(index, Qt.DisplayRole)
editor.setText(text)

def setModelData(self, editor, model, index):
print 'setting model data'
if editor.property("currentIndex") is not None:
return
model.setData(index, editor.currentText(), Qt.EditRole)

class TableModel(QAbstractTableModel):

def __init__(self, data=None, parent=None):
super(TableModel, self).__init__(parent)
if data is not None:
self.tbldata = data
else:
self.tbldata = [[]]

def rowCount(self, parent=QModelIndex()):
return len(self.tbldata)

def columnCount(self, parent=QModelIndex()):
return len(self.tbldata[0])

def data(self, index, role):
if role == Qt.DisplayRole:
return self.tbldata[index.row()][index.column()]

def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled

def setData(self, index, value, role):
if role == Qt.EditRole:
self.tbldata[index.row()][index.column()] = value
return True
return False

class Form(QDialog):

def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.setWindowTitle("Test mapping to QComboBox")
tbldata = [
["Carol", "The Lighthouse", 1],
["Donald", "47338 Park Ave.", 2],
["Emma", "Research Station", 0]
]
self.model = TableModel(tbldata)
self.view = QTableView() # you have a view, but you won't add it as a widget to the layout
self.view.setModel(self.model)

# create widgets
name_label = QLabel("Name:")
address_label = QLabel("Address:")
type_label = QLabel("Type:")
self.name_edit = QLineEdit()
self.address_edit = QLineEdit()
self.type_combo = QComboBox()
self.type_combo.addItems(["0", "1", "2"])
prev_button = QPushButton("Previous")
next_button = QPushButton("Next")
change_button = QPushButton("Change Data")

layout = QGridLayout()
layout.addWidget(name_label, 0, 0)
layout.addWidget(self.name_edit, 0, 1)
layout.addWidget(prev_button, 0, 2)
layout.addWidget(address_label, 1, 0)
layout.addWidget(self.address_edit, 1, 1)
layout.addWidget(next_button, 1, 2)
layout.addWidget(type_label, 2, 0)
layout.addWidget(self.type_combo, 2, 1)
layout.addWidget(change_button, 2, 2)
self.setLayout(layout)

# mapper
self.mapper = QDataWidgetMapper()
self.mapper.setModel(self.model)
self.delegate = CustomDelegate()
self.mapper.setItemDelegate(self.delegate)
self.mapper.addMapping(self.name_edit, 0)
self.mapper.addMapping(self.address_edit, 1)
self.mapper.addMapping(self.type_combo, 2)
self.mapper.setCurrentIndex(0)

# connections
prev_button.clicked.connect(self.goPrevRecord)
next_button.clicked.connect(self.goNextRecord)
change_button.clicked.connect(self.changeData)

def goPrevRecord(self):
self.mapper.toPrevious()

def goNextRecord(self):
self.mapper.toNext()

def changeData(self):
self.model.tbldata = [
["Paul", "72 Main St.", 2],
["John", "473 Wabash Ave.", 2],
["Jenny", "Outer Space", 0]
]

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()


Everything runs perfectly -- except that I'm unable to write data back to the model from the widgets! I put a print statement in the setModelData() method of the delegate to see whether the method is ever called, and it doesn't seem to be. And if it's not called, that would certainly explain why no edits go through to the model!

The Trolltech tutorial I linked to makes it a point that all I should need to re-implement are the setEditorData() and setModelData() methods, but it seems as though I am missing something elementary, and a few hours of searching (and continued tinkering) has turned up nothing. If anyone can help point me in the right direction, I would deeply appreciate it!

anda_skoa
8th March 2014, 09:12
I am not sure you need a delegate.

You problem seems to be that your model has an integer in the column that you map into the combobox, but you haven't set the mapping to the "currentIndex" property:


self.mapper.addMapping(self.type_combo, 2)

this creates a mapping between the type_combo widget and column 2 of the model, using the default property of the widget.
In the case of QComboBox that is "currentText".

What you could try instead of using a delegate is to map to the combobox "currentIndex" property:


self.mapper.addMapping(self.type_combo, 2, "currentIndex")


Cheers,
_

tristam
8th March 2014, 18:38
I am not sure you need a delegate.

You problem seems to be that your model has an integer in the column that you map into the combobox, but you haven't set the mapping to the "currentIndex" property:


self.mapper.addMapping(self.type_combo, 2)

this creates a mapping between the type_combo widget and column 2 of the model, using the default property of the widget.
In the case of QComboBox that is "currentText".

What you could try instead of using a delegate is to map to the combobox "currentIndex" property:


self.mapper.addMapping(self.type_combo, 2, "currentIndex")


Cheers,
_

Thank you for the reply. I only had integers there because I had meant to implement something akin to the example in the Trolltech tutorial I linked to (where the QComboBox has its own QStringListModel, and the delegate maps indexes to actual text-based choices) but became stuck on the problem with setModelData. The same tutorial makes the following point:


For widgets that only hold one piece of information, like QLineEdit and the others used in the previous example, using a widget mapper is straightforward. However, for widgets that present choices to the user, such as QComboBox, we need to think about how the list of choices is stored, and how the mapper will update the widget when a choice is made....To manage this relationship, we need to introduce another common model/view component: a delegate.

Fortunately, the setEditorData function appears to work well: the correct integers are set in the comboboxes when I run the script, and the correct text is set in the line edit widgets. The problem remains in writing back to the model...I assumed that any time any widget data is changed that the delegate would automatically invoke the setModelData function, but it seems that that's not the case, unless I'm screwing up the re-implementation somehow.

EDIT: On a whim I changed the parent class of my custom delegate to QItemDelegate rather than QAbstractItemDelegate (the former being a subclass of the latter), and now the setModelData() method works! Not sure why that is the case, but I'm happy to have this resolved!