问题
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_())
来源:https://stackoverflow.com/questions/53059449/qstyleditemdelegate-to-display-qcombobox-in-qtableview