2017-02-03 108 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的文檔中。

無論如何,這段代碼給了我預期的結果,所以已經是這樣了。瞭解它的工作原理只是一種獎勵! ^^如果有人有解釋,我會很有興趣知道。