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?
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
Merci pour l'indice, j'ai trouvé une solution (voir ci-dessous) –