我正在使用 PyQt5 (5.7.1) 和 Python 3.5 开发一个应用程序。我使用 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_())
排序时如何让选择跟随被选元素?
最佳答案
感谢 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())
编辑:出于未知原因,在最后发出 dataChanged 大大加快了排序速度。我尝试发送带有 layoutAboutToBeChanged 和 layoutChanged 的 LayoutChangedHint(例如 self.layoutChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHing) ),但我得到一个错误,这些信号不接受参数,考虑到签名,这很奇怪这些信号在 Qt5 的文档中有描述。
无论如何,这段代码给了我预期的结果,所以这已经是这样了。了解它为什么有效只是一个奖励! ^^ 如果有人有解释,我很想知道。
关于python - PyQt - 如何重新实现 QAbstractTableModel 排序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42028534/