Editable QComboBox with QAbstractTableModel

  |   Source

Dear Lazyweb,

I'm working on a personal PyQt4 project for my parents' office, and I need to autocomplete an editable QComboBox.

I can't use a standard Qt model (I need to store python objects in it), so I subclassed QAbstractTableModel.

Here it is, it's a fairly generic subclass of QAbstractTableModel, with variable number of columns:

class QGenericTableModel(QAbstractTableModel):
    def __init__ (self, columns, parent=None, *args):
        super(QGenericTableModel, self).__init__(parent, *args)
        self.columns = columns
        self.headers = {}
        self.table = []

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

    def columnCount(self, parent):
        return self.columns

    def row(self, row):
        return self.table[row]

    def setData(self, index, value, role):
        if index.isValid() and role == Qt.EditRole:
            row = index.row()

            t = self.table[row]
            if index.column() >= self.columns:
                return False
            if isinstance(value, QVariant):
                t[index.column()] = value.toString().simplified()
            else:
                t[index.column()] = value
            self.emit(SIGNAL('dataChanged'), index, index)
            return True
        return False

    def data(self, index, role = Qt.DisplayRole):
        if not index.isValid():
            return QVariant()

        if (index.row() >= len(self.table)) or (index.row() < 0):
            return QVariant()

        if role == Qt.DisplayRole:
            return QVariant(self.table[index.row()][index.column()])

        return QVariant()

    def insertRow(self, row, parent=QModelIndex()):
        self.insertRows(row, 1, parent)

    def insertRows(self, row, count, parent=QModelIndex()):
        self.beginInsertRows(parent, row, row+count-1)
        for i in xrange(count):
            self.table.insert(row, ['',]*self.columns)
        self.endInsertRows()
        return True

    def removeRow(self, row, parent=QModelIndex()):
        self.removeRows(row, 1, parent)

    def removeRows(self, row, count, parent=QModelIndex()):
        self.beginRemoveRows(parent, row, row+count-1)
        for i in reversed(xrange(count)):
            self.table.pop(row+i)
        self.endRemoveRows()
        return True

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled

        return super(QGenericTableModel, self).flags(index) | Qt.ItemIsEditable

    def headerData(self, column, orientation, role = Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return QVariant()

        if orientation == Qt.Horizontal:
            if column >= self.columns:
                return QVariant()
            return self.headers[column]

        return QVariant()

    def setHeaderData(self, column, orientation, value, role = Qt.EditRole):
        self.headers[column] = value

It might not be perfect, but it works, and it's all that matters at the moment (since it's my first serious PyQt4 project). Oh, well, it worked until I tried to use it with an editable QComboBox.

And here's the app code (for the full working code, insert the above class right after the imports):

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *

app = QApplication(sys.argv)

model = QStandardItemModel()
for i, word in enumerate(['saluton', 'gxis la revido', 'hello', 'goodbye']):
    item = QStandardItem(word)
    model.setItem(i, 0, item)

#model = QGenericTableModel(1)
#for i, word in enumerate(['saluton', 'gxis la revido', 'hello', 'goodbye']):
#    model.insertRow(i)
#    index = model.index(i, 0)
#    model.setData(index, word, Qt.EditRole)

combo = QComboBox()
filterModel = QSortFilterProxyModel(combo)
completer = QCompleter(combo)

filterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
filterModel.setSourceModel(model)
filterModel.setFilterKeyColumn(0)

completer.setModel(filterModel)
completer.setCompletionColumn(0)
completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)

combo.setEditable(True)
combo.setCompleter(completer)
combo.setModel(model)
combo.setModelColumn(0)

if combo.isEditable():
    app.connect(combo.lineEdit(), SIGNAL('textEdited(QString)'), filterModel.setFilterFixedString)

combo.setModel(model)
combo.setModelColumn(0)

combo.show()

sys.exit(app.exec_())

Now, try running it. It works well when I use a QStandardItemModel.

Then, try decommenting the part where I use my subclassed model: it just doesn't work: clicking on any item makes the QComboBox not change its currentIndex, and this only happens if the combobox is editable.

I suspect I'm forgetting to override some function in my model, but I don't know what exactly to override, and Google didn't help me. This is also backed up by the fact that the exact same behaviour shows up when, instead of a QComboBox, I try to autocomplete on a QLineEdit.

So, dear readers: can anyone explain what is happening? Thanks in advance!

UPDATE: it turned out that the culprit was the following bit inside data():

if role == Qt.DisplayRole:
    return QVariant(self.table[index.row()][index.column()])

Adding also Qt.EditRole to the list of possible choices fixed the bug. YAY!

Comments powered by Disqus
Contents © 2013 David Paleino - Powered by Nikola