PyQt - самый простой рабочий пример комбинированного списка внутри QTableView

Справочная информация: я не могу найти полный рабочий пример комбинированного списка внутри QTableView, Поэтому я написал этот код на основе нескольких других более надуманных примеров. Проблема, однако, в том, что в этом примере необходимо дважды щелкнуть на поле со списком, прежде чем он станет активным, а затем вы должны нажать еще раз, чтобы раскрыть его. Это не очень удобно для пользователя. Если я делаю не модель / представление вещей, используя QTableWidget, выпадающий список выпадает на первый клик.

Вопрос: Может ли кто-нибудь взглянуть на это и сказать мне, что нужно сделать, чтобы он реагировал так же, как QTableWidget? Кроме того, если что-то, что я делаю, не нужно, пожалуйста, укажите это тоже. Например, необходимо ли ссылаться на стиль приложения?

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_()

5 ответов

Использование 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()

Если кому-то интересно, ниже приведен тот же пример, модифицированный для PyQt5 и Python 3. Основные обновления включают в себя:

  • Python 3: super().__init__()
  • PyQt5: большинство классов находятся в QtWidgets; QtGui не нужен для этого примера
  • Model.setData: порядок ввода аргументов изменен на: index, value, role, а также True вернулся вместо None
  • Поле со списком choices и содержимое таблицы теперь указано внутри Main; это делает Delegate а также Model более общий
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.Qt.DisplayRole, QtCore.QVariant(value))
    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_()

Если вы пытаетесь настроить, когда представление отображает редактор, вам нужно изменить триггер редактирования, как определено в QAbstractItemView. По умолчанию это редактирование на doubleClick, но я думаю, что вы ищете QAbstractItemView.CurrentChanged. Установите это, вызывая myView.setEditTrigger()

Вы можете попробовать что-то вроде этого.

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_()

Как видите, я только что добавил пару строк в ваш код. Представление управляет "редакцией", поэтому вы должны изменить триггеры редакции. Затем, когда вы устанавливаете данные делегата, заставьте делегата показывать всплывающее окно из виджета.

Некоторое время назад я прочитал сообщение в блоге, в котором автор разделил QAbstractItemView на "правильную" работу с делегатами (издание, навигация, обновление данных и т. Д.), Но я не могу найти сообщение:(

Надеюсь, поможет.

Это должно работать:

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)

Вот моя версия для PyQt5. Он основан на работе ToddP( этот ответ) и решает еще несколько проблем:

  • Поддержка текста и значения
  • Использует Qt.EditRole обновить модель
  • Показывать всплывающее окно при нажатии на виджет

В общем, я считаю, что такой подход неудовлетворителен. Когда поле со списком получает фокус, оно начинает поглощать все виды событий. Нет четких указаний на то, что он находится в фокусе. Когда всплывающее окно отображается, вы больше не можете перейти к следующей ячейке. Это просто кажется неуклюжим по сравнению с реальным виджетом QComboBox внутри QTableWidget.

Поскольку у меня небольшая таблица (< 100 строк), я переключился на QTableWidget. Теперь я могу сам создавать виджеты и помещать их в таблицу, используяsetCellWidget(). Это значительно упрощает добавление особого поведения (например, переход к следующей / предыдущей ячейке при нажатии влево / вправо в начале / конце текста в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)

Вы захотите использовать

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

(см. этот ответ)

Смотрите также:

Другие вопросы по тегам