14

Background: I cannot find a full working example of a combobox inside a QTableView. So I wrote this code based on several other more contrived examples out there. The problem is, however, that this example requires you to double-click on the combobox before it becomes enabled, then you have to click again to drop it down. It's not very user-friendly. If I do the non-model/view-thing using QTableWidget, the combobox drops down on the first click.

Question: Can someone look at this and tell me what needs to be done to make it respond just like QTableWidget? Also if there is anything that I'm doing that is unnecessary, please indicate that also. For example, is it absolutely necessary to reference the application style?

import sys
from PyQt4 import QtGui, QtCore

rows = "ABCD"
choices = ['apple', 'orange', 'banana']

class Delegate(QtGui.QItemDelegate):
    def __init__(self, owner, items):
        super(Delegate, self).__init__(owner)
        self.items = items
    def createEditor(self, parent, option, index):
        self.editor = QtGui.QComboBox(parent)
        self.editor.addItems(self.items)
        return self.editor
    def paint(self, painter, option, index):
        value = index.data(QtCore.Qt.DisplayRole).toString()
        style = QtGui.QApplication.style()
        opt = QtGui.QStyleOptionComboBox()
        opt.text = str(value)
        opt.rect = option.rect
        style.drawComplexControl(QtGui.QStyle.CC_ComboBox, opt, painter)
        QtGui.QItemDelegate.paint(self, painter, option, index)
    def setEditorData(self, editor, index):
        value = index.data(QtCore.Qt.DisplayRole).toString()
        num = self.items.index(value)
        editor.setCurrentIndex(num)
    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, QtCore.Qt.DisplayRole, QtCore.QVariant(value))
    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

class Model(QtCore.QAbstractTableModel):
    def __init__(self):
        super(Model, self).__init__()
        self.table = [[row, choices[0]] for row in rows]
    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self.table)
    def columnCount(self, index=QtCore.QModelIndex()):
        return 2
    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            return self.table[index.row()][index.column()]
    def setData(self, index, role, value):
        if role == QtCore.Qt.DisplayRole:
            self.table[index.row()][index.column()] = value

class Main(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Main, self).__init__(parent)
        self.model = Model()
        self.table = QtGui.QTableView()
        self.table.setModel(self.model)
        self.table.setItemDelegateForColumn(1, Delegate(self, ["apple", "orange", "banana"]))
        self.setCentralWidget(self.table)
        self.setWindowTitle('Delegate Test')
        self.show()

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    main = Main()
    app.exec_()
user2120303
  • 255
  • 2
  • 4
  • 8
  • You may find my answer to [this question](http://stackoverflow.com/questions/17615997/pyqt-how-to-set-qcombobox-in-a-table-view-using-qitemdelegate) helpful. – Pavel Strakhov Aug 05 '13 at 21:56
  • 2
    Thank you, now I see that the `paint` override is unnecessary, and I need to `openPersistentEditor`. But calling `openPersistentEditor` seems to defeat the purpose of model/view if I need to call that from outside of the model. Plus, it seems inefficient to draw all those comboboxes when you can only operate one at a time. Is there a way to either get rid of the double-click requirement so it appears upon cell selection? – user2120303 Aug 05 '13 at 22:23
  • You don't need to call it from the model. You can use another object (e.g. your subclassed view or form) to track model changing and invoke editor if necessary. For the second question, connect the `selectionChanged` signal of the `view->selectionModel()` to your slot. In this slot open editor in selected cell and close previous editors if necessary. – Pavel Strakhov Aug 06 '13 at 08:06

6 Answers6

7

Using QTableWiget.setCellWidget

import sys
from PyQt4 import QtGui
app = QtGui.QApplication(sys.argv)
table = QtGui.QTableWidget(1,1)
combobox = QtGui.QComboBox()
combobox.addItem("Combobox item")
table.setCellWidget(0,0, combobox)
table.show()
app.exec()
Ramchandra Apte
  • 4,033
  • 2
  • 24
  • 44
6

If anyone is interested, below is the same example modified for PyQt5 and Python 3. Key updates include:

  • Python 3: super().__init__()
  • PyQt5: most classes are in QtWidgets; QtGui is not needed for this example
  • Model.setData: input argument order changed to: index, value, role, and True returned instead of None
  • Combo box choices and table contents now specified inside Main; this makes Delegate and Model more general
from PyQt5 import QtWidgets, QtCore

class Delegate(QtWidgets.QItemDelegate):
    def __init__(self, owner, choices):
        super().__init__(owner)
        self.items = choices
    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QComboBox(parent)
        self.editor.addItems(self.items)
        return self.editor
    def paint(self, painter, option, index):
        value = index.data(QtCore.Qt.DisplayRole)
        style = QtWidgets.QApplication.style()
        opt = QtWidgets.QStyleOptionComboBox()
        opt.text = str(value)
        opt.rect = option.rect
        style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt, painter)
        QtWidgets.QItemDelegate.paint(self, painter, option, index)
    def setEditorData(self, editor, index):
        value = index.data(QtCore.Qt.DisplayRole)
        num = self.items.index(value)
        editor.setCurrentIndex(num)
    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, QtCore.QVariant(value), QtCore.Qt.DisplayRole)
    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

class Model(QtCore.QAbstractTableModel):
    def __init__(self, table):
        super().__init__()
        self.table = table
    def rowCount(self, parent):
        return len(self.table)
    def columnCount(self, parent):
        return len(self.table[0])
    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            return self.table[index.row()][index.column()]
    def setData(self, index, value, role):
        if role == QtCore.Qt.EditRole:
            self.table[index.row()][index.column()] = value
        return True

class Main(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        # set combo box choices:
        choices = ['apple', 'orange', 'banana']
        # create table data:
        table   = []
        table.append(['A', choices[0]])
        table.append(['B', choices[0]])
        table.append(['C', choices[0]])
        table.append(['D', choices[0]])
        # create table view:
        self.model     = Model(table)
        self.tableView = QtWidgets.QTableView()
        self.tableView.setModel(self.model)
        self.tableView.setItemDelegateForColumn(1, Delegate(self,choices))
        # make combo boxes editable with a single-click:
        for row in range( len(table) ):
            self.tableView.openPersistentEditor(self.model.index(row, 1))
        # initialize
        self.setCentralWidget(self.tableView)
        self.setWindowTitle('Delegate Test')
        self.show()

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    main = Main()
    app.exec_()
ToddP
  • 652
  • 13
  • 18
  • While this works it has graphical glitches when changing the size of the column (instead of continuously changing the size of the ComboBox, the ComboBox stays while on top while another ComboBox is being drawn in the background - this ComboBox shows the expected behavior so what is needed is just to remove the top combo box). – JKAbrams Aug 24 '20 at 01:20
1

If you are trying to adjust when the view displays the editor, you need to change the edit trigger as defined in QAbstractItemView. The default is edit on doubleClick, but I think what you are after is QAbstractItemView.CurrentChanged. Set it by calling myView.setEditTrigger()

Robot Inc
  • 51
  • 4
1

You can try something like this.

import sys
from PyQt4 import QtGui, QtCore

rows = "ABCD"
choices = ['apple', 'orange', 'banana']

class Delegate(QtGui.QItemDelegate):
    def __init__(self, owner, items):
        super(Delegate, self).__init__(owner)
        self.items = items
    def createEditor(self, parent, option, index):
        self.editor = QtGui.QComboBox(parent)
        self.editor.addItems(self.items)
        return self.editor
    def paint(self, painter, option, index):
        value = index.data(QtCore.Qt.DisplayRole).toString()
        style = QtGui.QApplication.style()
        opt = QtGui.QStyleOptionComboBox()
        opt.text = str(value)
        opt.rect = option.rect
        style.drawComplexControl(QtGui.QStyle.CC_ComboBox, opt, painter)
        QtGui.QItemDelegate.paint(self, painter, option, index)
    def setEditorData(self, editor, index):
        value = index.data(QtCore.Qt.DisplayRole).toString()
        num = self.items.index(value)
        editor.setCurrentIndex(num)
        if index.column() == 1: #just to be sure that we have a QCombobox
            editor.showPopup()
    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, QtCore.Qt.DisplayRole, QtCore.QVariant(value))
    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

class Model(QtCore.QAbstractTableModel):
    def __init__(self):
        super(Model, self).__init__()
        self.table = [[row, choices[0]] for row in rows]
    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self.table)
    def columnCount(self, index=QtCore.QModelIndex()):
        return 2
    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
    def data(self, index, role):
        if role == QtCore.Qt.DisplayRole:
            return self.table[index.row()][index.column()]
    def setData(self, index, role, value):
        if role == QtCore.Qt.DisplayRole:
            self.table[index.row()][index.column()] = value
            return True
        else:
            return False

class Main(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Main, self).__init__(parent)
        self.model = Model()
        self.table = QtGui.QTableView()
        self.table.setModel(self.model)
        self.table.setEditTriggers(QtGui.QAbstractItemView.CurrentChanged) # this is the one that fits best to your request
        self.table.setItemDelegateForColumn(1, Delegate(self, ["apple", "orange", "banana"]))
        self.setCentralWidget(self.table)
        self.setWindowTitle('Delegate Test')
        self.show()

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    main = Main()
    app.exec_()

As you can see, I just added a couple of lines to your code. The View manages the "edition" so you must change the edition triggers. Then when you set the delegate data, force the delegate to show the popup from the widget.

Some time ago, I read a blog post on which the author subclassed QAbstractItemView in order to work 'properly' with delegates (edition, navigation, updating data, etc), but I cant find the post :(

Hope it helps.

user20679
  • 442
  • 4
  • 17
0

This should work:

view = QTreeView()
model = QStandardItemModel(view)
view.setModel(model)

combobox = QComboBox()

child1 = QStandardItem('test1')
child2 = QStandardItem('test2')
child3 = QStandardItem('test3')
model.appendRow([child1, child2, child3])
a = model.index(0, 2)
view.setIndexWidget(a, combobox)
avasygn
  • 103
  • 1
  • 7
0

Here is my version for PyQt5. It's based on ToddP's work (this answer) and solves a few more problems:

  • Support for text and value
  • Uses Qt.EditRole to update the model
  • Show the popup as soon as the widget is clicked

All in all, I found the user experience of this approach to be bad. When the combox box gets focus, it starts to swallow all kinds of events. There is no clear indication that it has the focus. When the popup is visible, you can no longer tab to the next cell. It just feels clunky when compared to a real QComboBox widget inside of a QTableWidget.

Since I have just a short table (< 100 rows), I've switched to QTableWidget. I can now create the widgets myself and put them into the table using setCellWidget(). That makes it much easier to add special behavior (like going to the next/previous cell when pressing left/right at the start/end of the text in a QLineEdit).

class ComboBoxDelegate(QtWidgets.QItemDelegate):
    def __init__(self, choices, parent=None):
        super().__init__(parent)

        self.choices = choices
        self.valueIndex = {
            self.choices[i][1]: i
            for i in range(len(self.choices))
        }

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QComboBox(parent)
        for text, value in self.choices:
            self.editor.addItem(text, value)
        QTimer.singleShot(0, self.showPopup)
        return self.editor

    @QtCore.pyqtSlot()
    def showPopup(self):
        self.editor.showPopup()

    def paint(self, painter, option, index):
        value = index.data(QtCore.Qt.DisplayRole)
        style = QtWidgets.QApplication.style()
        opt = QtWidgets.QStyleOptionComboBox()
        opt.text = str(value)
        opt.rect = option.rect
        style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt, painter)
        style.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter)
        QtWidgets.QItemDelegate.paint(self, painter, option, index)

    def setEditorData(self, editor, index):
        value = index.data(QtCore.Qt.EditRole)
        num = self.valueIndex[value]
        editor.setCurrentIndex(num)

    def setModelData(self, editor, model, index):
        value = editor.currentData()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

You will want to to use

table.setEditTriggers(QAbstractItemView.CurrentChanged) # Edit on first click

(see this answer)

See also:

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820