使用组合委托对 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())