Montag, 10. November 2014

Dynamic Delegates in PyQt4

I wanted to create a table allowing the presentation of and input for different attributes. This meant the table had to contain different input elements like combo boxes, lists, calendars, etc.. These could also occur together in one cell and act as one input element, i.e. to allow the user to extend an existing list (see "Ressource type" in the image below).


The framwork to be used: PyQt4.

The concept provided by Qt4 is to use delegates. These allow the developer to modify the behaviour from each cell in a table. The problem is that delegates only can handle one input element, not multiple.

My solution:

An sbstract delegate which enherits from the QItemDelegate.
The paint function has to be overwritten, since if not the default presentation is also painted additional to your custom presentation. Resulting in two elements painted on top of each other. Here I just skip the default presentation for elements in the second column, since I do not want use custom delegates for the labels in the the column 0 (my field attribute labels).
If the data in the cell changes the setModelData function is called. Additionally to calling the parent function I am emitting a signal that the data changed. This function is overwritten by delegates consisting of multiple input elements but is used by elements with one input element.
class AbstractDfDelegate(QtGui.QItemDelegate):

    def paint(self, parent, option, index):
        if index.column() == 1:
            pass
        else:
            QtGui.QItemDelegate.paint(self, parent, option, index)
           
    def setModelData(self, editor, model, index):
        QtGui.QItemDelegate.setModelData(self, editor, model, index)
        model.emit(QtCore.SIGNAL("itemChanged(QStandardItem)"), model.itemFromIndex(index))
The use case mentioned in the beginning is the extendable list. It accepts default values which are used as the input for the list element.
class ListDelegate(AbstractDfDelegate):
       
    def __init__(self, defaults, parent=None):
        self._defaults = defaults
        AbstractDfDelegate.__init__(self, parent)

First the input elements are defined. One list element and a text editor. They are displayed in a vertical layout which again is used by a QWidget, which is also the widget we use as editor from now on.
    def createEditor(self, parent, option, index):
        if index.column() == 1:
            self._index = index
           
            self._listBox = QtGui.QListView()
            self._listBox.setSelectionMode(QtGui.QListWidget.MultiSelection)
            self._listBox.setModel(QtGui.QStringListModel(self._defaults))
            self._listBox.clearSelection()
            self._listBox.setMinimumHeight(100)
            self._listBox.setMaximumHeight(100)
           
            self._textEditor = QtGui.QLineEdit()
          
            if QtCore.Qt.NoItemFlags == self._index.flags():
                self._listBox.setEnabled(False)
                self._textEditor.setEnabled(False)
           
            self.connect(self._listBox.selectionModel(), QtCore.SIGNAL("selectionChanged(QItemSelection,QItemSelection)"), self.setModelData)
            self.connect(self._textEditor, QtCore.SIGNAL("textChanged(QString)"), self.setModelData)

            vLayout = QtGui.QVBoxLayout(parent)
            vLayout.addWidget(self._listBox)
            vLayout.addWidget(self._textEditor)
            vLayout.setMargin(0)
           
            widget = QtGui.QWidget(parent)
            widget.setLayout(vLayout)

            return widget   
The setModelData function of the abstract delegate is overwritten. The selected values of the list are collected and the text of the text field is added if there is any. The resulting value is then set manually in the model. Again after collecting the value a signal is emitted that the model was updated.
    def setModelData(self, *args):
        values = list()
        for y in self._listBox.selectedIndexes():
            values.append(unicode(y.data().toString().trimmed()))
        additionalText = unicode(self._textEditor.text().trimmed()).replace(",", "\,")
        if len(additionalText) > 0:
            values.append(additionalText)

        self._index.model().setData(self._index, values)
        self._index.model().emit(QtCore.SIGNAL("itemChanged(QStandardItem)"), self._index.model().itemFromIndex(self._index))       
The last function is the setEditorData function which presents the value of the model in the editor. Therefore the value is split up. To decide which values are from the list the values have to be compared with the default values. All remaining values are presented in the text box.
    def setEditorData(self, editor, index):
        if index.column() == 1 and not editor is None:
            items = index.model().data(index).toStringList()
            additionalItems = list()
            for item in items:
                if item in self._defaults:
                    i = self._defaults.index(item)
                    self._listBox.selectionModel().select(self._listBox.model().index(i, 0), QtGui.QItemSelectionModel.Select)
                else:
                    additionalItems.append(str(item))
            self._textEditor.setText(",".join(additionalItems))
        else:
            return AbstractDfDelegate.setEditorData(self, editor, index)
The signal emitted by the setModelData function is connected to a slot function handling the change as well as the table model:
self.connect(self.tableView.model(), QtCore.SIGNAL("itemChanged(QStandardItem)"), self.__propertyTableValueChangedSlot)
In the slot function I then extract the name and the value of the property and can do with those what I need to do:
def __propertyTableValueChangedSlot(self, item):
        """ Slot to handle value changes. """
      
        propertyName = unicode(item.model().item(item.row(), item.column()-1).text())
        propertyValue = item.index().data().toPyObject()

Keine Kommentare:

Kommentar veröffentlichen