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

时间:2017-02-03 16:16:32

标签: python sorting pyqt5 qsortfilterproxymodel

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

我尝试将QAbstractTableModel与QSortFilterProxyModel一起使用,重新实现QSortFilterProxyModel.filterAcceptsRow()以进行多列过滤(请参阅此博文:http://www.dayofthenewdan.com/2013/02/09/Qt_QSortFilterProxyModel.html)。但是因为每行调用此方法,所以当存在大量行时,过滤非常慢。

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

import pandas as pd
from PyQt5 import QtCore, QtWidgets


class PandasTableModel(QtCore.QAbstractTableModel):

    def __init__(self,  parent=None, *args):
        super(PandasTableModel,  self).__init__(parent,  *args)
        self._filters = {}
        self._sortBy = []
        self._sortDirection = []
        self._dfSource = pd.DataFrame()
        self._dfDisplay = pd.DataFrame()

    def rowCount(self,  parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dfDisplay.shape[0]

    def columnCount(self,  parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dfDisplay.shape[1]

    def data(self, index, role):
        if index.isValid() and role == QtCore.Qt.DisplayRole:
            return QtCore.QVariant(self._dfDisplay.values[index.row()][index.column()])
        return QtCore.QVariant()

    def headerData(self, col, orientation=QtCore.Qt.Horizontal, role=QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return QtCore.QVariant(str(self._dfDisplay.columns[col]))
        return QtCore.QVariant()

    def setupModel(self, header, data):
        self._dfSource = pd.DataFrame(data, columns=header)
        self._sortBy = []
        self._sortDirection = []
        self.setFilters({})

    def setFilters(self, filters):
        self.modelAboutToBeReset.emit()
        self._filters = filters
        self.updateDisplay()
        self.modelReset.emit()

    def sort(self, col, order=QtCore.Qt.AscendingOrder):
        #self.layoutAboutToBeChanged.emit()
        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()
        #self.layoutChanged.emit()
        self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

    def updateDisplay(self):

        dfDisplay = self._dfSource.copy()

        # Filtering
        cond = pd.Series(True, index = dfDisplay.index)
        for column, value in self._filters.items():
            cond = cond & \
                (dfDisplay[column].str.lower().str.find(str(value).lower()) >= 0)
        dfDisplay = dfDisplay[cond]

        # Sorting
        if len(self._sortBy) != 0:
            dfDisplay.sort_values(by=self._sortBy,
                                ascending=self._sortDirection,
                                inplace=True)

        # Updating
        self._dfDisplay = dfDisplay

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

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

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

class Ui_TableFilteringDialog(object):
    def setupUi(self, TableFilteringDialog):
        TableFilteringDialog.setObjectName("TableFilteringDialog")
        TableFilteringDialog.resize(400, 300)
        self.verticalLayout = QtWidgets.QVBoxLayout(TableFilteringDialog)
        self.verticalLayout.setObjectName("verticalLayout")
        self.tableView = QtWidgets.QTableView(TableFilteringDialog)
        self.tableView.setObjectName("tableView")
        self.tableView.setSortingEnabled(True)
        self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.verticalLayout.addWidget(self.tableView)
        self.groupBox = QtWidgets.QGroupBox(TableFilteringDialog)
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.formLayout = QtWidgets.QFormLayout()
        self.formLayout.setObjectName("formLayout")
        self.column1Label = QtWidgets.QLabel(self.groupBox)
        self.column1Label.setObjectName("column1Label")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.column1Label)
        self.column1Field = QtWidgets.QLineEdit(self.groupBox)
        self.column1Field.setObjectName("column1Field")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.column1Field)
        self.column2Label = QtWidgets.QLabel(self.groupBox)
        self.column2Label.setObjectName("column2Label")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.column2Label)
        self.column2Field = QtWidgets.QLineEdit(self.groupBox)
        self.column2Field.setObjectName("column2Field")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.column2Field)
        self.verticalLayout_2.addLayout(self.formLayout)
        self.verticalLayout.addWidget(self.groupBox)

        self.retranslateUi(TableFilteringDialog)
        QtCore.QMetaObject.connectSlotsByName(TableFilteringDialog)

    def retranslateUi(self, TableFilteringDialog):
        _translate = QtCore.QCoreApplication.translate
        TableFilteringDialog.setWindowTitle(_translate("TableFilteringDialog", "Dialog"))
        self.groupBox.setTitle(_translate("TableFilteringDialog", "Filters"))
        self.column1Label.setText(_translate("TableFilteringDialog", "Name"))
        self.column2Label.setText(_translate("TableFilteringDialog", "Occupation"))

class TableFilteringDialog(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(TableFilteringDialog, self).__init__(parent)

        self.ui = Ui_TableFilteringDialog()
        self.ui.setupUi(self)

        self.tableModel = PandasTableModel()
        header = ['Name', 'Occupation']
        data = [
            ['Abe', 'President'],
            ['Angela', 'Chancelor'],
            ['Donald', 'President'],
            ['François', 'President'],
            ['Jinping', 'President'],
            ['Justin', 'Prime minister'],
            ['Theresa', 'Prime minister'],
            ['Vladimir', 'President'],
            ['Donald', 'Duck']
        ]
        self.tableModel.setupModel(header, data)
        self.ui.tableView.setModel(self.tableModel)

        self.ui.column1Field.textEdited.connect(self.filtersEdited)
        self.ui.column2Field.textEdited.connect(self.filtersEdited)

    def filtersEdited(self):
        filters = {}
        values = [
            self.ui.column1Field.text().lower(),
            self.ui.column2Field.text().lower()
        ]
        for col, value in enumerate(values):
            if value == '':
                continue
            column = self.tableModel.headerData(col, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole).value()
            filters[column]=value
        self.tableModel.setFilters(filters)



if __name__ == '__main__':

    import sys
    app = QtWidgets.QApplication(sys.argv)

    dialog = TableFilteringDialog()
    dialog.show()

    sys.exit(app.exec_()) 

如何在排序时使选择符合所选元素?

1 个答案:

答案 0 :(得分:1)

感谢ekhumoro,我找到了解决方案。 sort函数应该存储持久索引,创建新索引并更改它们。这是执行此操作的代码。很多记录看起来排序速度有点慢,但这是可以接受的。

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())

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

无论如何,这段代码给了我预期的结果,所以已经这样了。了解它的工作原理只是一个奖励! ^^如果有人有解释,我有兴趣知道。

相关问题