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 中绘制整行的背景?