2017-02-03 343 views
1

我正在用Python 3.5开发一个使用PyQt5(5.7.1)的应用程序。我使用QTableView来显示一长串记录(超过10,000)。我希望能够在同一时间对多个列进行排序和过滤。PyQt - 如何重新实现QAbstractTableModel排序?

我尝试使用带有QSortFilterProxyModel一个QAbstractTableModel,重新实现QSortFilterProxyModel.filterAcceptsRow()具有多列过滤(见本博客文章:http://www.dayofthenewdan.com/2013/02/09/Qt_QSortFilterProxyModel.html)。但由于每行都调用此方法,因此在有大量行时过滤非常缓慢。

我以为使用熊猫进行过滤可以提高性能。所以,我创建了以下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_()) 

我怎样才能让排序时选择按照选定的元素:

可以使用下面的示例中测试这种模式?

+0

您需要更新持久模型索引,视图使用这些索引来跟踪选定和扩展的项目。查看[QAbstractItemModel:Subclassing]的最后几段(https://doc.qt.io/qt-5/qabstractitemmodel.html#subclassing)。这没有简单的解决方案。 – ekhumoro

+0

感谢您的提示,我找到了一个解决方案(见下文) –

回答

1

感谢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(例如self.layoutChanged.emit([],QtCore.QAbstractItemModel.VerticalSortHing))发送一个LayoutChangedHint,但是我得到一个错误信息,这些信号没有参数,这是奇怪的考虑签名这些信号描述在Qt5的文档中。

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