使用组合委托对 QTableView 进行排序会删除 delgetes,双击后除外
Sorting QTableView with combo delegates removes delgetes except after double clicking
我有 QTableView,它查看 pandas table。第一行有 QComboBox 代表。当我按列对 table 进行排序时,代表消失了。
下面是我的代码的一个工作示例。
import sys
import pandas as pd
import numpy as np
from PyQt5.QtCore import (QAbstractTableModel, Qt, pyqtProperty, pyqtSlot,
QVariant, QModelIndex)
from PyQt5.QtWidgets import (QItemDelegate, QComboBox, QMainWindow, QTableView,
QApplication)
class DataFrameModel(QAbstractTableModel):
DtypeRole = Qt.UserRole + 1000
ValueRole = Qt.UserRole + 1001
ActiveRole = Qt.UserRole + 1
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
@pyqtSlot(int, Qt.Orientation, result=str)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
try:
return self._dataframe.columns.tolist()[section]
except (IndexError, ):
return QVariant()
elif orientation == Qt.Vertical:
try:
if section in [0]:
pass
else:
return self._dataframe.index.tolist()[section - 1]
except (IndexError, ):
return QVariant()
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount()
and 0 <= index.column() <
self.columnCount()):
return QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == Qt.DisplayRole:
return str(val)
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QVariant()
def roleNames(self):
roles = {
Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
def setData(self, index, value, role):
col = index.column()
row = index.row()
if index.row() == 0:
if isinstance(value, QVariant):
value = value.value()
if hasattr(value, 'toPyObject'):
value = value.toPyObject()
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
else:
try:
value = eval(value)
if not isinstance(
value,
self._dataframe.applymap(type).iloc[row, col]):
value = self._dataframe.iloc[row, col]
except Exception as e:
value = self._dataframe.iloc[row, col]
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
return True
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
col_name = self._dataframe.columns.tolist()[column]
sheet1 = self._dataframe.iloc[:1, :]
sheet2 = self._dataframe.iloc[1:, :].sort_values(
col_name, ascending=order == Qt.AscendingOrder, inplace=False)
sheet2.reset_index(drop=True, inplace=True)
sheet3 = pd.concat([sheet1, sheet2], ignore_index=True)
self.setDataFrame(sheet3)
self.layoutChanged.emit()
class ComboBoxDelegate(QItemDelegate):
def __init__(self, owner, choices):
super().__init__(owner)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QItemDelegate):
self.parent().openPersistentEditor(0, index)
QItemDelegate.paint(self, painter, option, index)
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)
@pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class MainWindow(QMainWindow):
def __init__(self, pandas_sheet):
super().__init__()
self.pandas_sheet = pandas_sheet
for i in range(1):
self.pandas_sheet.loc[-1] = [''] * len(self.pandas_sheet.columns.values)
self.pandas_sheet.index = self.pandas_sheet.index + 1
self.pandas_sheet = self.pandas_sheet.sort_index()
self.table = QTableView()
self.setCentralWidget(self.table)
delegate = ComboBoxDelegate(self.table,
['m1', 'm2', 'm3'])
model = DataFrameModel(self.pandas_sheet, self)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.setItemDelegateForRow(0, delegate)
for i in range(model.columnCount()):
ix = model.index(0, i)
self.table.openPersistentEditor(ix)
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
if __name__ == '__main__':
df = pd.DataFrame({'a': ['col0'] * 5,
'b': np.arange(5),
'c': np.random.rand(5)})
app = QApplication(sys.argv)
window = MainWindow(df)
window.show()
sys.exit(app.exec_())
下图显示排序前的table。
按其中一列对 table 进行排序后的下图。
我希望第一行的样式在按任何列排序前后都相同。这可能吗?
默认情况下,不会显示编辑器,除非用户使用分配给 editTriggers 的标志中指示的事件与项目交互,或者如果您使用 openPersistentEditor() 强制他们打开它们。
考虑到最后一个选项,您可以自动执行显示的任务,但为此必须可以从委托的 paint 方法访问视图,因为它总是被解决方案调用以将其作为父项传递(看来您是试图实现)并使用 openPersistentEditor() 如果它是视图,在你的情况下会出现错误,因为父级不是 QItemDelegate 但继承自 QAbstractItemView,此外你必须传递 QModelIndex。
综合以上,解决方案是:
def paint(self, painter, option, index):
if isinstance(self.parent(), <b>QAbstractItemView</b>):
<b>self.parent().openPersistentEditor(index)</b>
因此,每次重新绘制委托时(例如排序后),都会调用 openPersistentEditor() 使编辑器可见。
更新:
编辑器必须通过 setModelData 将信息保存在 QModelIndex 角色中,并使用 setEditorData 检索它们,在您的情况下您没有实现第二个,因此编辑器在再次创建编辑器时将不会获取信息。此外,setModelData 将信息保存在 Qt::EditRole 中,但在您的模型中它不处理该角色,因此您必须使用 Qt::DisplayRole.
综合以上,解决方案是:
class ComboBoxDelegate(QItemDelegate):
def __init__(self, parent, choices):
super().__init__(parent)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
def setModelData(self, editor, model, index):
value = editor.currentText()
model.setData(index, value, Qt.DisplayRole)
def setEditorData(self, editor, index):
text = index.data(Qt.DisplayRole) or ""
editor.setCurrentText(text)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
@pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
我有 QTableView,它查看 pandas table。第一行有 QComboBox 代表。当我按列对 table 进行排序时,代表消失了。
下面是我的代码的一个工作示例。
import sys
import pandas as pd
import numpy as np
from PyQt5.QtCore import (QAbstractTableModel, Qt, pyqtProperty, pyqtSlot,
QVariant, QModelIndex)
from PyQt5.QtWidgets import (QItemDelegate, QComboBox, QMainWindow, QTableView,
QApplication)
class DataFrameModel(QAbstractTableModel):
DtypeRole = Qt.UserRole + 1000
ValueRole = Qt.UserRole + 1001
ActiveRole = Qt.UserRole + 1
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)
@pyqtSlot(int, Qt.Orientation, result=str)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
try:
return self._dataframe.columns.tolist()[section]
except (IndexError, ):
return QVariant()
elif orientation == Qt.Vertical:
try:
if section in [0]:
pass
else:
return self._dataframe.index.tolist()[section - 1]
except (IndexError, ):
return QVariant()
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or not (0 <= index.row() < self.rowCount()
and 0 <= index.column() <
self.columnCount()):
return QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == Qt.DisplayRole:
return str(val)
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QVariant()
def roleNames(self):
roles = {
Qt.DisplayRole: b'display',
DataFrameModel.DtypeRole: b'dtype',
DataFrameModel.ValueRole: b'value'
}
return roles
def setData(self, index, value, role):
col = index.column()
row = index.row()
if index.row() == 0:
if isinstance(value, QVariant):
value = value.value()
if hasattr(value, 'toPyObject'):
value = value.toPyObject()
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
else:
try:
value = eval(value)
if not isinstance(
value,
self._dataframe.applymap(type).iloc[row, col]):
value = self._dataframe.iloc[row, col]
except Exception as e:
value = self._dataframe.iloc[row, col]
self._dataframe.iloc[row, col] = value
self.dataChanged.emit(index, index, (Qt.DisplayRole,))
return True
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
col_name = self._dataframe.columns.tolist()[column]
sheet1 = self._dataframe.iloc[:1, :]
sheet2 = self._dataframe.iloc[1:, :].sort_values(
col_name, ascending=order == Qt.AscendingOrder, inplace=False)
sheet2.reset_index(drop=True, inplace=True)
sheet3 = pd.concat([sheet1, sheet2], ignore_index=True)
self.setDataFrame(sheet3)
self.layoutChanged.emit()
class ComboBoxDelegate(QItemDelegate):
def __init__(self, owner, choices):
super().__init__(owner)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QItemDelegate):
self.parent().openPersistentEditor(0, index)
QItemDelegate.paint(self, painter, option, index)
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)
@pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class MainWindow(QMainWindow):
def __init__(self, pandas_sheet):
super().__init__()
self.pandas_sheet = pandas_sheet
for i in range(1):
self.pandas_sheet.loc[-1] = [''] * len(self.pandas_sheet.columns.values)
self.pandas_sheet.index = self.pandas_sheet.index + 1
self.pandas_sheet = self.pandas_sheet.sort_index()
self.table = QTableView()
self.setCentralWidget(self.table)
delegate = ComboBoxDelegate(self.table,
['m1', 'm2', 'm3'])
model = DataFrameModel(self.pandas_sheet, self)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.setItemDelegateForRow(0, delegate)
for i in range(model.columnCount()):
ix = model.index(0, i)
self.table.openPersistentEditor(ix)
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
if __name__ == '__main__':
df = pd.DataFrame({'a': ['col0'] * 5,
'b': np.arange(5),
'c': np.random.rand(5)})
app = QApplication(sys.argv)
window = MainWindow(df)
window.show()
sys.exit(app.exec_())
下图显示排序前的table。
按其中一列对 table 进行排序后的下图。
我希望第一行的样式在按任何列排序前后都相同。这可能吗?
默认情况下,不会显示编辑器,除非用户使用分配给 editTriggers 的标志中指示的事件与项目交互,或者如果您使用 openPersistentEditor() 强制他们打开它们。
考虑到最后一个选项,您可以自动执行显示的任务,但为此必须可以从委托的 paint 方法访问视图,因为它总是被解决方案调用以将其作为父项传递(看来您是试图实现)并使用 openPersistentEditor() 如果它是视图,在你的情况下会出现错误,因为父级不是 QItemDelegate 但继承自 QAbstractItemView,此外你必须传递 QModelIndex。
综合以上,解决方案是:
def paint(self, painter, option, index):
if isinstance(self.parent(), <b>QAbstractItemView</b>):
<b>self.parent().openPersistentEditor(index)</b>
因此,每次重新绘制委托时(例如排序后),都会调用 openPersistentEditor() 使编辑器可见。
更新:
编辑器必须通过 setModelData 将信息保存在 QModelIndex 角色中,并使用 setEditorData 检索它们,在您的情况下您没有实现第二个,因此编辑器在再次创建编辑器时将不会获取信息。此外,setModelData 将信息保存在 Qt::EditRole 中,但在您的模型中它不处理该角色,因此您必须使用 Qt::DisplayRole.
综合以上,解决方案是:
class ComboBoxDelegate(QItemDelegate):
def __init__(self, parent, choices):
super().__init__(parent)
self.items = choices
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(self.items)
editor.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
def setModelData(self, editor, model, index):
value = editor.currentText()
model.setData(index, value, Qt.DisplayRole)
def setEditorData(self, editor, index):
text = index.data(Qt.DisplayRole) or ""
editor.setCurrentText(text)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
@pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())