I have QTableView which views a pandas table. The first row has QComboBox delegates. When I sort the table by a column the delegates disappear.
A working example of my code is below.
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_())
The image below shows the table before sorting.
The image below after sorting the table by one of the columns.
I would like to have the style for the first row the same before and after sorting by any column. Is this possible?