2017-02-03 3 views
1

Ich entwickle eine Anwendung mit PyQt5 (5.7.1) mit Python 3.5. Ich verwende eine QTableView, um eine lange Liste von Aufzeichnungen (mehr als 10.000) anzuzeigen. Ich möchte in der Lage sein, diese Liste in mehreren Spalten gleichzeitig zu sortieren und zu filtern.PyQt - Wie QAbstractTableModel Sortierung neu zu implementieren?

Ich versuchte, einen QAbstractTableModel mit einem QSortFilterProxyModel verwenden, Neuimplementierung QSortFilterProxyModel.filterAcceptsRow() mehrspaltigen Filterung zu haben (siehe diese Blog-Post: http://www.dayofthenewdan.com/2013/02/09/Qt_QSortFilterProxyModel.html). Da diese Methode jedoch für jede Zeile aufgerufen wird, ist die Filterung bei einer großen Anzahl von Zeilen sehr langsam.

Ich dachte, mit Pandas für die Filterung könnte die Leistung verbessern. Also habe ich die folgende PandasTableModel-Klasse, die in der Tat mehrspaltigen Filterung durchführen kann sehr schnell auch bei einer großen Anzahl von Zeilen sowie Sortierung:

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 

Diese Klasse repliziert das Verhalten eines QSortFilterProxyModel, mit Ausnahme von ein Aspekt. Wenn ein Element in der Tabelle in QTableView ausgewählt wird, wirkt sich die Sortierung der Tabelle nicht auf die Auswahl aus (wenn z. B. die erste Zeile vor der Sortierung ausgewählt wird, wird die erste Zeile nach der Sortierung immer noch ausgewählt und nicht die gleiche Zeile wie zuvor.

Ich denke, das Problem hängt mit den Signalen zusammen, die ausgesendet werden.Für die Filterung habe ich modelAboutToBeReset() und modelReset() verwendet, aber diese Signale löschen die Auswahl in QTableView, so dass sie nicht zum Sortieren geeignet sind (How to update QAbstractTableModel and QTableView after sorting the data source?)) dass layoutAboutToBeChanged() und layoutChanged() ausgegeben werden sollten, QTableView wird jedoch nicht aktualisiert, wenn ich diese Signale nutze (ich verstehe nicht warum). Wenn ich dataChanged() aussetze, sobald die Sortierung abgeschlossen ist, wird QTableView aktualisiert mit dem oben beschriebenen Verhalten (Auswahl nicht aktualisiert).

Sie können dieses Modell testen Sie das folgende Beispiel verwenden:

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

Wie kann ich die Auswahl machen das ausgewählte Element folgen beim Sortieren?

+0

Sie müssen die persistenten Modellindizes aktualisieren, die von Sichten verwendet werden, um ausgewählte und erweiterte Elemente zu verfolgen. Siehe die letzten Absätze von [QAbstractItemModel: Subclassing] (https://doc.qt.io/qt-5/qabstractitemmodel.html#subclassing). Dafür gibt es keine einfache Lösung. – ekhumoro

+0

Danke für den Hinweis, ich habe eine Lösung gefunden (siehe unten) –

Antwort

1

Dank ekhumoro habe ich eine Lösung gefunden. Die Sortierfunktion sollte die persistenten Indizes speichern, neue Indizes erstellen und sie ändern. Hier ist der Code dafür. Es scheint ein wenig langsamer mit vielen Platten zu sortieren, aber das ist akzeptabel.

edit: aus einem unbekannten grund, emittieren daten am ende geändert beschleunigt die sortierung erheblich. Ich habe versucht, einen LayoutChangedHint mit layoutAboutToBeChanged und layoutChanged (zB self.layoutChanged.emit ([], QtCore.QAbstractItemModel.VerticalSortHing)) zu senden, aber ich bekomme einen Fehler, dass diese Signale keine Argumente annehmen, was seltsam ist, wenn man die Signatur von Diese in Qt5's Doc.

Wie auch immer, dieser Code gibt mir das erwartete Ergebnis, also ist das schon so. Zu verstehen, warum es funktioniert, ist nur ein Bonus! ^^ Wenn jemand eine Erklärung hat, würde mich das interessieren.

Verwandte Themen