-->

QStyledItemDelegate to display QComboBox in QTable

2020-07-20 04:17发布

问题:

I'm new to Python and PyQt5. I'm using QStyledItemDelegate to make one of the QTableView column consisting of only ComboBox. I managed to display the ComboBox but and I'm having trouble with its behavior.

Problem 1: The ComboBox doesn't seems to commit changes to the model even though selection was changed. I used the export button to print out the list for checking purposes.

Problem 2: When I add a new row to the table, the new row ComboBox selection keeps reverting back to the first selection. Why is that?

Could anyone help me with some advice? Thank you.

Code:

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:

By default, setModelData() is called when the editor is closed, in your case when using openPersistentEditor() the editor will never be closed unless you call closePersistentEditor(), therefore setModelData() will not be called. So the solution for the above is to issue the commitData() signal, so we notify the delegate to save the data. But still it does not save the data because the implementation of setData() has problems, in your code you use range(0, 1) and it is known that range(0, n) is [0, 1, ..., n-1] so in your case range(0, 1) equals [0] and the data of the QComboBox is in column 1, so you have to modify that logic so that it also accepts the 1.

On the other hand the error that I see is that if a row is added the editor is not opened persistently, and the logic is that the code: if isinstance(self.parent(), QtWidgets.QAbstractItemView): self.parent().openPersistentEditor (index) Do that job, but the delegate's parent is expected to be the view, not the mainwidow.

Using the above, the following solution is obtained:

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