QStyledItemDelegate 在 QTableView 中显示 QComboBox

Posted

技术标签:

【中文标题】QStyledItemDelegate 在 QTableView 中显示 QComboBox【英文标题】:QStyledItemDelegate to display QComboBox in QTableView 【发布时间】:2018-10-30 07:39:29 【问题描述】:

我是 Python 和 PyQt5 的新手。我正在使用QStyledItemDelegate 制作仅包含ComboBox 的QTableView 列之一。我设法显示了 ComboBox,但我遇到了它的行为问题。

问题 1:即使选择已更改,ComboBox 似乎也没有提交对模型的更改。我使用导出按钮打印出列表以供检查。

问题 2:当我向表中添加新行时,新行 ComboBox 选择不断恢复为第一个选择。这是为什么呢?

谁能帮我一些建议?谢谢。

代码:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import re

class Delegate(QStyledItemDelegate):
    def __init__(self, owner, choices):
        super().__init__(owner)
        self.items = choices

    def createEditor(self, parent, option, index):
        editor = QComboBox(parent)
        editor.addItems(self.items)
        return editor

    def paint(self, painter, option, index):
        if isinstance(self.parent(), QAbstractItemView):
            self.parent().openPersistentEditor(index, 1)
        QStyledItemDelegate.paint(self, painter, option, index)

    def setEditorData(self, editor, index):
        editor.blockSignals(True)
        value = index.data(Qt.DisplayRole)
        num = self.items.index(value)
        editor.setCurrentIndex(num)
        editor.blockSignals(False)

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

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

class Model(QAbstractTableModel):
    ActiveRole = Qt.UserRole + 1
    def __init__(self, datain, headerdata, parent=None):
        """
        Args:
            datain: a list of lists\n
            headerdata: a list of strings
        """
        super().__init__()
        self.arraydata = datain
        self.headerdata = headerdata

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return QVariant(self.headerdata[section])
        return QVariant()

    def rowCount(self, parent):
        return len(self.arraydata)

    def columnCount(self, parent):
        if len(self.arraydata) > 0:
            return len(self.arraydata[0])
        return 0

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

    def data(self, index, role):
        if not index.isValid():
            return QVariant()
        elif role != Qt.DisplayRole:
            return QVariant()
        return QVariant(self.arraydata[index.row()][index.column()])

    def setData(self, index, value, role):
        r = re.compile(r"^[0-9]\d*(\.\d+)?$")
        if role == Qt.EditRole and value != "":
            if not index.column() in range(0, 1):
                if index.column() == 2:
                    if r.match(value) and (0 < float(value) <= 1):
                        self.arraydata[index.row()][index.column()] = value
                        self.dataChanged.emit(index, index, (Qt.DisplayRole, ))
                        return True
                else:
                    if r.match(value):
                        self.arraydata[index.row()][index.column()] = value
                        self.dataChanged.emit(index, index, (Qt.DisplayRole, ))
                        return True
            elif index.column() in range(0, 1):
                self.arraydata[index.row()][index.column()] = value
                self.dataChanged.emit(index, index, (Qt.DisplayRole, ))
                return True
        return False

    def print_arraydata(self):
        print(self.arraydata)

class Main(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        # create table view:
        self.get_choices_data()
        self.get_table_data()
        self.tableview = self.createTable()
        self.tableview.clicked.connect(self.tv_clicked_pos)

        # Set the maximum value of row to the selected row
        self.selectrow = self.tableview.model().rowCount(QModelIndex())

        # create buttons:
        self.addbtn = QPushButton('Add')
        self.addbtn.clicked.connect(self.insert_row)
        self.deletebtn = QPushButton('Delete')
        self.deletebtn.clicked.connect(self.remove_row)
        self.exportbtn = QPushButton('Export')
        self.exportbtn.clicked.connect(self.export_tv)
        self.computebtn = QPushButton('Compute')
        self.enablechkbox = QCheckBox('Completed')

        # create label:
        self.lbltitle = QLabel('Table')
        self.lbltitle.setFont(QFont('Arial', 20))

        # create gridlayout
        self.grid_layout = QGridLayout()
        self.grid_layout.addWidget(self.exportbtn, 2, 2, 1, 1)
        self.grid_layout.addWidget(self.computebtn, 2, 3, 1, 1)
        self.grid_layout.addWidget(self.addbtn, 2, 4, 1, 1)
        self.grid_layout.addWidget(self.deletebtn, 2, 5, 1, 1)
        self.grid_layout.addWidget(self.enablechkbox, 2, 6, 1, 1, Qt.AlignCenter)
        self.grid_layout.addWidget(self.tableview, 1, 0, 1, 7)
        self.grid_layout.addWidget(self.lbltitle, 0, 3, 1, 1, Qt.AlignCenter)

        # initializing layout
        self.title = 'Data Visualization Tool'
        self.setWindowTitle(self.title)
        self.setGeometry(0, 0, 1024, 576)
        self.showMaximized()
        self.centralwidget = QWidget()
        self.centralwidget.setLayout(self.grid_layout)
        self.setCentralWidget(self.centralwidget)

    def get_table_data(self): 
        # set initial table values:
        self.tabledata = [['Name', self.choices[0], 0.0, 0.0, 0.0]]

    def get_choices_data(self):
        # set combo box choices:
        self.choices = ['type_1', 'type_2', 'type_3', 'type_4', 'type_5']

    def createTable(self):
        tv = QTableView()

        # set header for columns:
        header = ['Name', 'Type', 'var1', 'var2', 'var3']       

        tablemodel = Model(self.tabledata, header, self)
        tv.setModel(tablemodel)
        hh = tv.horizontalHeader()
        tv.resizeRowsToContents()

        # ItemDelegate for combo boxes
        tv.setItemDelegateForColumn(1, Delegate(self, self.choices))

        # make combo boxes editable with a single-click:
        for row in range(len(self.tabledata)):
            tv.openPersistentEditor(tablemodel.index(row, 1))

        return tv

    def export_tv(self):
        self.tableview.model().print_arraydata()

    def insert_row(self, position, rows=1, index=QModelIndex()):
        position = self.selectrow
        self.tableview.model().beginInsertRows(QModelIndex(), position, position + rows - 1)
        for row in range(rows):
            self.tableview.model().arraydata.append(['Name', self.choices[0], 0.0, 0.0, 0.0])
        self.tableview.model().endInsertRows()
        self.tableview.model().rowsInserted.connect(lambda: QTimer.singleShot(0, self.tableview.scrollToBottom))
        return True

    def remove_row(self, position, rows=1, index=QModelIndex()):
        position = self.selectrow
        self.tableview.model().beginRemoveRows(QModelIndex(), position, position + rows - 1)
        self.tableview.model().arraydata = self.tableview.model().arraydata[:position] + self.tableview.model().arraydata[position + rows:]
        self.tableview.model().endRemoveRows()
        return True

    def tv_clicked_pos(self, indexClicked):
        self.selectrow = indexClicked.row()

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

【问题讨论】:

【参考方案1】:

默认情况下,关闭编辑器时会调用setModelData(),在您使用openPersistentEditor() 的情况下,除非您调用closePersistentEditor(),否则编辑器将永远不会关闭,因此不会调用setModelData()。所以上面的解决方案是发出commitData()信号,所以我们通知委托保存数据。但它仍然没有保存数据,因为setData() 的实现有问题,在你的代码中你使用range(0, 1) 并且已知range(0, n)[0, 1, ..., n-1] 所以在你的情况下range(0, 1) 等于[0]QComboBox 的数据在 1 列中,因此您必须修改该逻辑,使其也接受 1

另一方面,我看到的错误是,如果添加一行,编辑器不会持久打开,逻辑是代码:if isinstance(self.parent(), QtWidgets.QAbstractItemView): self.parent().openPersistentEditor (index) 做这项工作,但委托的父母应该是视图,而不是主寡妇。

利用上述,得到如下解决方案:

from PyQt5 import QtCore, QtGui, QtWidgets
import re

class Delegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, owner, choices):
        super().__init__(owner)
        self.items = choices

    def paint(self, painter, option, index):
        if isinstance(self.parent(), QtWidgets.QAbstractItemView):
            self.parent().openPersistentEditor(index)
        super(Delegate, self).paint(painter, option, index)

    def createEditor(self, parent, option, index):
        editor = QtWidgets.QComboBox(parent)
        editor.currentIndexChanged.connect(self.commit_editor)
        editor.addItems(self.items)
        return editor

    def commit_editor(self):
        editor = self.sender()
        self.commitData.emit(editor)

    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, value, QtCore.Qt.EditRole)

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

class Model(QtCore.QAbstractTableModel):
    ActiveRole = QtCore.Qt.UserRole + 1
    def __init__(self, datain, headerdata, parent=None):
        """
        Args:
            datain: a list of lists\n
            headerdata: a list of strings
        """
        super().__init__()
        self.arraydata = datain
        self.headerdata = headerdata

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
            return QtCore.QVariant(self.headerdata[section])
        return QtCore.QVariant()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid(): return 0
        return len(self.arraydata)

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid(): return 0
        if len(self.arraydata) > 0:
            return len(self.arraydata[0])
        return 0

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

    def data(self, index, role):
        if not index.isValid():
            return QtCore.QVariant()
        elif role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()
        return QtCore.QVariant(self.arraydata[index.row()][index.column()])

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        r = re.compile(r"^[0-9]\d*(\.\d+)?$")
        if role == QtCore.Qt.EditRole and value != "" and 0 < index.column() < self.columnCount():
            if index.column() in (0, 1):
                self.arraydata[index.row()][index.column()] = value
                self.dataChanged.emit(index, index, (QtCore.Qt.DisplayRole, ))
                return True
            else:
                if index.column() == 2:
                    if r.match(value) and (0 < float(value) <= 1):
                        self.arraydata[index.row()][index.column()] = value
                        self.dataChanged.emit(index, index, (QtCore.Qt.DisplayRole, ))
                        return True
                else:
                    if r.match(value):
                        self.arraydata[index.row()][index.column()] = value
                        self.dataChanged.emit(index, index, (QtCore.Qt.DisplayRole, ))
                        return True
        return False

    def print_arraydata(self):
        print(self.arraydata)

    def insert_row(self, data, position, rows=1):
        self.beginInsertRows(QtCore.QModelIndex(), position, position + rows - 1)
        for i, e in enumerate(data):
            self.arraydata.insert(i+position, e[:])
        self.endInsertRows()
        return True

    def remove_row(self, position, rows=1):
        self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1)
        self.arraydata = self.arraydata[:position] + self.arraydata[position + rows:]
        self.endRemoveRows()
        return True

    def append_row(self, data):
        self.insert_row([data], self.rowCount())


class Main(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        # create table view:
        self.get_choices_data()
        self.get_table_data()
        self.tableview = self.createTable()
        self.tableview.model().rowsInserted.connect(lambda: QtCore.QTimer.singleShot(0, self.tableview.scrollToBottom))

        # Set the maximum value of row to the selected row
        self.selectrow = self.tableview.model().rowCount()

        # create buttons:
        self.addbtn = QtWidgets.QPushButton('Add')
        self.addbtn.clicked.connect(self.insert_row)
        self.deletebtn = QtWidgets.QPushButton('Delete')
        self.deletebtn.clicked.connect(self.remove_row)
        self.exportbtn = QtWidgets.QPushButton('Export')
        self.exportbtn.clicked.connect(self.export_tv)
        self.computebtn = QtWidgets.QPushButton('Compute')
        self.enablechkbox = QtWidgets.QCheckBox('Completed')

        # create label:
        self.lbltitle = QtWidgets.QLabel('Table')
        self.lbltitle.setFont(QtGui.QFont('Arial', 20))

        # create gridlayout
        grid_layout = QtWidgets.QGridLayout()
        grid_layout.addWidget(self.exportbtn, 2, 2, 1, 1)
        grid_layout.addWidget(self.computebtn, 2, 3, 1, 1)
        grid_layout.addWidget(self.addbtn, 2, 4, 1, 1)
        grid_layout.addWidget(self.deletebtn, 2, 5, 1, 1)
        grid_layout.addWidget(self.enablechkbox, 2, 6, 1, 1, QtCore.Qt.AlignCenter)
        grid_layout.addWidget(self.tableview, 1, 0, 1, 7)
        grid_layout.addWidget(self.lbltitle, 0, 3, 1, 1, QtCore.Qt.AlignCenter)

        # initializing layout
        self.title = 'Data Visualization Tool'
        self.setWindowTitle(self.title)
        self.setGeometry(0, 0, 1024, 576)
        self.showMaximized()
        self.centralwidget = QtWidgets.QWidget()
        self.centralwidget.setLayout(grid_layout)
        self.setCentralWidget(self.centralwidget)

    def get_table_data(self): 
        # set initial table values:
        self.tabledata = [['Name', self.choices[0], 0.0, 0.0, 0.0]]

    def get_choices_data(self):
        # set combo box choices:
        self.choices = ['type_1', 'type_2', 'type_3', 'type_4', 'type_5']

    def createTable(self):
        tv = QtWidgets.QTableView()
        # set header for columns:
        header = ['Name', 'Type', 'var1', 'var2', 'var3']       

        tablemodel = Model(self.tabledata, header, self)
        tv.setModel(tablemodel)
        hh = tv.horizontalHeader()
        tv.resizeRowsToContents()
        # ItemDelegate for combo boxes
        tv.setItemDelegateForColumn(1, Delegate(tv, self.choices))
        return tv

    def export_tv(self):
        self.tableview.model().print_arraydata()

    def remove_row(self):
        r = self.tableview.currentIndex().row()
        self.tableview.model().remove_row(r)

    def insert_row(self):
        self.tableview.model().append_row(['Name', self.choices[0], 0.0, 0.0, 0.0])


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    main = Main()
    main.show()
    sys.exit(app.exec_())

【讨论】:

非常感谢您的解释和发现我的错误。它现在工作得更好,但 ComboBox 上的代码仍然存在问题。我相信它是paint(),与索引有关,我不确定。例如[Type-1] [Type-2] [Type-3] 你尝试添加一行,索引指向[Type-2],绘制结果错误。 @malco 你什么时候创建一个行来指示应该在 QComboBox 中默认选择哪些数据? @malco 我看到每次添加一行时,QComboBox 的默认值都是 Type_1,因为这是 insert_row 中所指示的 是的,没错。但是尽管arraydata 是正确的,但 ComboBox 不显示 Type_1。顺便说一句,新行总是添加到表的末尾。 @malco 好奇怪,我没观察到,我看到默认显示Type_1,你是用我的代码还是你修改过?

以上是关于QStyledItemDelegate 在 QTableView 中显示 QComboBox的主要内容,如果未能解决你的问题,请参考以下文章

在带有 QSqlQueryModel 的 QListView 上使用 QStyledItemDelegate

无法在 QStyledItemDelegate 中绘制复选框

如何在 QStyledItemDelegate 中绘制整行的背景?

PyQt:在 QStyledItemDelegate 中使用 QTextEdit 作为编辑器

正确实现 QStyledItemDelegate

QStyledItemDelegate 绘制自定义小部件失败