2017-02-03 1 views
1

Je développe une application en utilisant PyQt5 (5.7.1) avec Python 3.5. J'utilise un QTableView pour afficher une longue liste d'enregistrements (plus de 10 000). Je veux pouvoir trier et filtrer cette liste sur plusieurs colonnes en même temps.PyQt - Comment réimplémenter le tri QAbstractTableModel?

J'ai essayé d'utiliser un QAbstractTableModel avec un (QSortFilterProxyModel, réimplémentant QSortFilterProxyModel.filterAcceptsRow) d'avoir un filtrage multicolumn (voir ce billet de blog: http://www.dayofthenewdan.com/2013/02/09/Qt_QSortFilterProxyModel.html). mais comme cette méthode est appelée pour chaque ligne, le filtrage est très lent lorsqu'il y a un grand nombre de lignes. Je pensais que l'utilisation de Pandas pour le filtrage pourrait améliorer les performances. Donc, j'ai créé la classe PandasTableModel suivante, qui peut en effet effectuer un filtrage multicolumn très rapidement, même avec un grand nombre de lignes, ainsi que le tri:

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 

Cette classe reproduit le comportement d'un QSortFilterProxyModel, à l'exception d'un aspect. Si un élément de la table est sélectionné dans QTableView, le tri de la table n'affecte pas la sélection (par exemple, si la première ligne est sélectionnée avant le tri, la première ligne sera toujours sélectionnée après le tri, et non la même que précédemment)

Je pense que le problème est lié aux signaux qui sont émis.Pour le filtrage, j'ai utilisé modelAboutToBeReset() et modelReset(), mais ces signaux annulent la sélection dans QTableView, donc ils ne sont pas adaptés pour le tri.J'y lis (How to update QAbstractTableModel and QTableView after sorting the data source?) que layoutAboutToBeChanged() et layoutChanged() doivent être émises, mais QTableView ne se met pas à jour si j'utilise ces signaux (je ne comprends pas pourquoi) En émettant dataChanged() une fois le tri terminé, QTableView est mis à jour, mais avec le comportement décrit ci-dessus (sélection non mise à jour)

Vous pouvez tester ce modèle en utilisant l'exemple suivant:

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

Comment puis-je faire la sélection suivre l'élément sélectionné lors du tri?

+0

Vous devez mettre à jour les index de modèle persistants, qui sont utilisés par les vues pour suivre les éléments sélectionnés et développés. Voir les derniers paragraphes de [QAbstractItemModel: Subclassing] (https://doc.qt.io/qt-5/qabstractitemmodel.html#subclassing). Il n'y a pas de solution simple pour cela. – ekhumoro

+0

Merci pour l'indice, j'ai trouvé une solution (voir ci-dessous) –

Répondre

1

Grâce à ekhumoro, j'ai trouvé une solution. La fonction de tri doit stocker les index persistants, créer de nouveaux index et les modifier. Voici le code pour le faire. Cela semble un peu plus lent avec beaucoup de disques, mais c'est acceptable.

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

edit: pour une raison inconnue, l'émission de dataChanged à la fin accélère considérablement le tri. J'ai essayé d'envoyer un LayoutChangedHint avec layoutAboutToBeChanged et layoutChanged (par exemple self.layoutChanged.emit ([], QtCore.QAbstractItemModel.VerticalSortHing)), mais j'obtiens une erreur que ces signaux ne prennent pas d'arguments, ce qui est étrange compte tenu de la signature de ces signaux décrits dans le document de Qt5.

De toute façon, ce code me donne le résultat attendu, c'est déjà ça. Comprendre pourquoi cela fonctionne n'est qu'un bonus! ^^ Si quelqu'un a une explication, je serais intéressé de savoir cependant.