PyQt - 如何重新实现 QAbstractTableModel 排序?

Gui*_* A. 6 python sorting qsortfilterproxymodel pyqt5

我正在使用 PyQt5 (5.7.1) 和 Python 3.5 开发一个应用程序。我使用 QTableView 来显示一长串记录(超过 10,000 条)。我希望能够同时在多个列上对这个列表进行排序和过滤。

\n\n

我尝试将 QAbstractTableModel 与 QSortFilterProxyModel 一起使用,重新实现 QSortFilterProxyModel.filterAcceptsRow() 以进行多列过滤(请参阅此博客文章: http: //www.dayofthenewdan.com/2013/02/09/Qt_QSortFilterProxyModel.html)。但由于每行都会调用此方法,因此当行数较多时,过滤速度非常慢。

\n\n

我认为使用 Pandas 进行过滤可以提高性能。所以我创建了以下 PandasTableModel 类,即使有大量行,它确实可以非常快速地执行多列过滤以及排序:

\n\n
import pandas as pd\nfrom PyQt5 import QtCore, QtWidgets\n\n\nclass PandasTableModel(QtCore.QAbstractTableModel):\n\n    def __init__(self,  parent=None, *args):\n        super(PandasTableModel,  self).__init__(parent,  *args)\n        self._filters = {}\n        self._sortBy = []\n        self._sortDirection = []\n        self._dfSource = pd.DataFrame()\n        self._dfDisplay = pd.DataFrame()\n\n    def rowCount(self,  parent=QtCore.QModelIndex()):\n        if parent.isValid():\n            return 0\n        return self._dfDisplay.shape[0]\n\n    def columnCount(self,  parent=QtCore.QModelIndex()):\n        if parent.isValid():\n            return 0\n        return self._dfDisplay.shape[1]\n\n    def data(self, index, role):\n        if index.isValid() and role == QtCore.Qt.DisplayRole:\n            return QtCore.QVariant(self._dfDisplay.values[index.row()][index.column()])\n        return QtCore.QVariant()\n\n    def headerData(self, col, orientation=QtCore.Qt.Horizontal, role=QtCore.Qt.DisplayRole):\n        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:\n            return QtCore.QVariant(str(self._dfDisplay.columns[col]))\n        return QtCore.QVariant()\n\n    def setupModel(self, header, data):\n        self._dfSource = pd.DataFrame(data, columns=header)\n        self._sortBy = []\n        self._sortDirection = []\n        self.setFilters({})\n\n    def setFilters(self, filters):\n        self.modelAboutToBeReset.emit()\n        self._filters = filters\n        self.updateDisplay()\n        self.modelReset.emit()\n\n    def sort(self, col, order=QtCore.Qt.AscendingOrder):\n        #self.layoutAboutToBeChanged.emit()\n        column = self._dfDisplay.columns[col]\n        ascending = (order == QtCore.Qt.AscendingOrder)\n        if column in self._sortBy:\n            i = self._sortBy.index(column)\n            self._sortBy.pop(i)\n            self._sortDirection.pop(i)\n        self._sortBy.insert(0, column)\n        self._sortDirection.insert(0, ascending)\n        self.updateDisplay()\n        #self.layoutChanged.emit()\n        self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())\n\n    def updateDisplay(self):\n\n        dfDisplay = self._dfSource.copy()\n\n        # Filtering\n        cond = pd.Series(True, index = dfDisplay.index)\n        for column, value in self._filters.items():\n            cond = cond & \\\n                (dfDisplay[column].str.lower().str.find(str(value).lower()) >= 0)\n        dfDisplay = dfDisplay[cond]\n\n        # Sorting\n        if len(self._sortBy) != 0:\n            dfDisplay.sort_values(by=self._sortBy,\n                                ascending=self._sortDirection,\n                                inplace=True)\n\n        # Updating\n        self._dfDisplay = dfDisplay\n
Run Code Online (Sandbox Code Playgroud)\n\n

除了一个方面之外,此类复制了 QSortFilterProxyModel 的行为。如果在QTableView中选择了表格中的一项,对表格进行排序不会影响选择(例如,如果在排序之前选择了第一行,则排序后仍然会选择第一行,而不是与之前相同。

\n\n

我认为问题与发出的信号有关。对于过滤,我使用了 modelAboutToBeReset() 和 modelReset(),但这些信号取消了 QTableView 中的选择,因此它们不适合排序。我在那里读到(How to update QAbstractTableModel and QTableView after sorting the data source?)应该发出layoutAboutToBeChanged()和layoutChanged()。但是,如果我使用这些信号,QTableView 不会更新(我实际上不明白为什么)。当排序完成后发出 dataChanged() 时,QTableView 会更新,但具有上述行为(选择未更新)。

\n\n

您可以使用以下示例测试该模型:

\n\n
class Ui_TableFilteringDialog(object):\n    def setupUi(self, TableFilteringDialog):\n        TableFilteringDialog.setObjectName("TableFilteringDialog")\n        TableFilteringDialog.resize(400, 300)\n        self.verticalLayout = QtWidgets.QVBoxLayout(TableFilteringDialog)\n        self.verticalLayout.setObjectName("verticalLayout")\n        self.tableView = QtWidgets.QTableView(TableFilteringDialog)\n        self.tableView.setObjectName("tableView")\n        self.tableView.setSortingEnabled(True)\n        self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\n        self.verticalLayout.addWidget(self.tableView)\n        self.groupBox = QtWidgets.QGroupBox(TableFilteringDialog)\n        self.groupBox.setObjectName("groupBox")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.verticalLayout_2.setObjectName("verticalLayout_2")\n        self.formLayout = QtWidgets.QFormLayout()\n        self.formLayout.setObjectName("formLayout")\n        self.column1Label = QtWidgets.QLabel(self.groupBox)\n        self.column1Label.setObjectName("column1Label")\n        self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.column1Label)\n        self.column1Field = QtWidgets.QLineEdit(self.groupBox)\n        self.column1Field.setObjectName("column1Field")\n        self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.column1Field)\n        self.column2Label = QtWidgets.QLabel(self.groupBox)\n        self.column2Label.setObjectName("column2Label")\n        self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.column2Label)\n        self.column2Field = QtWidgets.QLineEdit(self.groupBox)\n        self.column2Field.setObjectName("column2Field")\n        self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.column2Field)\n        self.verticalLayout_2.addLayout(self.formLayout)\n        self.verticalLayout.addWidget(self.groupBox)\n\n        self.retranslateUi(TableFilteringDialog)\n        QtCore.QMetaObject.connectSlotsByName(TableFilteringDialog)\n\n    def retranslateUi(self, TableFilteringDialog):\n        _translate = QtCore.QCoreApplication.translate\n        TableFilteringDialog.setWindowTitle(_translate("TableFilteringDialog", "Dialog"))\n        self.groupBox.setTitle(_translate("TableFilteringDialog", "Filters"))\n        self.column1Label.setText(_translate("TableFilteringDialog", "Name"))\n        self.column2Label.setText(_translate("TableFilteringDialog", "Occupation"))\n\nclass TableFilteringDialog(QtWidgets.QDialog):\n\n    def __init__(self, parent=None):\n        super(TableFilteringDialog, self).__init__(parent)\n\n        self.ui = Ui_TableFilteringDialog()\n        self.ui.setupUi(self)\n\n        self.tableModel = PandasTableModel()\n        header = [\'Name\', \'Occupation\']\n        data = [\n            [\'Abe\', \'President\'],\n            [\'Angela\', \'Chancelor\'],\n            [\'Donald\', \'President\'],\n            [\'Fran\xc3\xa7ois\', \'President\'],\n            [\'Jinping\', \'President\'],\n            [\'Justin\', \'Prime minister\'],\n            [\'Theresa\', \'Prime minister\'],\n            [\'Vladimir\', \'President\'],\n            [\'Donald\', \'Duck\']\n        ]\n        self.tableModel.setupModel(header, data)\n        self.ui.tableView.setModel(self.tableModel)\n\n        self.ui.column1Field.textEdited.connect(self.filtersEdited)\n        self.ui.column2Field.textEdited.connect(self.filtersEdited)\n\n    def filtersEdited(self):\n        filters = {}\n        values = [\n            self.ui.column1Field.text().lower(),\n            self.ui.column2Field.text().lower()\n        ]\n        for col, value in enumerate(values):\n            if value == \'\':\n                continue\n            column = self.tableModel.headerData(col, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole).value()\n            filters[column]=value\n        self.tableModel.setFilters(filters)\n\n\n\nif __name__ == \'__main__\':\n\n    import sys\n    app = QtWidgets.QApplication(sys.argv)\n\n    dialog = TableFilteringDialog()\n    dialog.show()\n\n    sys.exit(app.exec_()) \n
Run Code Online (Sandbox Code Playgroud)\n\n

排序时如何使选择跟随所选元素?

\n

Gui*_* A. 4

感谢 ekhumoro,我找到了解决方案。排序函数应该存储持久索引、创建新索引并更改它们。这是执行此操作的代码。对于大量记录,排序似乎有点慢,但这是可以接受的。

def sort(self, col, order=QtCore.Qt.AscendingOrder):

    # Storing persistent indexes
    self.layoutAboutToBeChanged.emit()
    oldIndexList = self.persistentIndexList()
    oldIds = self._dfDisplay.index.copy()

    # Sorting data
    column = self._dfDisplay.columns[col]
    ascending = (order == QtCore.Qt.AscendingOrder)
    if column in self._sortBy:
        i = self._sortBy.index(column)
        self._sortBy.pop(i)
        self._sortDirection.pop(i)
    self._sortBy.insert(0, column)
    self._sortDirection.insert(0, ascending)
    self.updateDisplay()

    # Updating persistent indexes
    newIds = self._dfDisplay.index
    newIndexList = []
    for index in oldIndexList:
        id = oldIds[index.row()]
        newRow = newIds.get_loc(id)
        newIndexList.append(self.index(newRow, index.column(), index.parent()))
    self.changePersistentIndexList(oldIndexList, newIndexList)
    self.layoutChanged.emit()
    self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
Run Code Online (Sandbox Code Playgroud)

编辑:由于未知的原因,最后发出 dataChanged 会大大加快排序速度。我尝试发送带有layoutAboutToBeChanged和layoutChanged的LayoutChangedHint(例如 self.layoutChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHing) ),但我收到一个错误,这些信号不接受参数,考虑到签名的签名,这很奇怪这些信号在 Qt5 的文档中描述。

无论如何,这段代码给了我预期的结果,所以这已经是这样了。理解它为什么有效只是一个额外的好处!^^ 如果有人有解释,我很想知道。