How to create filters for QTableView in PyQt

28,579

Solution 1

Here is an example of filtering in PyQt using QSortFilterProxyModel, QStandardItemModel and QTableView, it can be easily adapted to other views and models:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from PyQt4 import QtCore, QtGui

class myWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)
        self.centralwidget  = QtGui.QWidget(self)
        self.lineEdit       = QtGui.QLineEdit(self.centralwidget)
        self.view           = QtGui.QTableView(self.centralwidget)
        self.comboBox       = QtGui.QComboBox(self.centralwidget)
        self.label          = QtGui.QLabel(self.centralwidget)

        self.gridLayout = QtGui.QGridLayout(self.centralwidget)
        self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
        self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
        self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)

        self.setCentralWidget(self.centralwidget)
        self.label.setText("Regex Filter")

        self.model = QtGui.QStandardItemModel(self)

        for rowName in range(3) * 5:
            self.model.invisibleRootItem().appendRow(
                [   QtGui.QStandardItem("row {0} col {1}".format(rowName, column))    
                    for column in range(3)
                    ]
                )

        self.proxy = QtGui.QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)

        self.view.setModel(self.proxy)
        self.comboBox.addItems(["Column {0}".format(x) for x in range(self.model.columnCount())])

        self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
        self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)

        self.horizontalHeader = self.view.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)

    @QtCore.pyqtSlot(int)
    def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
        self.logicalIndex   = logicalIndex
        self.menuValues     = QtGui.QMenu(self)
        self.signalMapper   = QtCore.QSignalMapper(self)  

        self.comboBox.blockSignals(True)
        self.comboBox.setCurrentIndex(self.logicalIndex)
        self.comboBox.blockSignals(True)

        valuesUnique = [    self.model.item(row, self.logicalIndex).text()
                            for row in range(self.model.rowCount())
                            ]

        actionAll = QtGui.QAction("All", self)
        actionAll.triggered.connect(self.on_actionAll_triggered)
        self.menuValues.addAction(actionAll)
        self.menuValues.addSeparator()

        for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):              
            action = QtGui.QAction(actionName, self)
            self.signalMapper.setMapping(action, actionNumber)  
            action.triggered.connect(self.signalMapper.map)  
            self.menuValues.addAction(action)

        self.signalMapper.mapped.connect(self.on_signalMapper_mapped)  

        headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())        

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)

        self.menuValues.exec_(QtCore.QPoint(posX, posY))

    @QtCore.pyqtSlot()
    def on_actionAll_triggered(self):
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  "",
                                        QtCore.Qt.CaseInsensitive,
                                        QtCore.QRegExp.RegExp
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(int)
    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  stringAction,
                                        QtCore.Qt.CaseSensitive,
                                        QtCore.QRegExp.FixedString
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(str)
    def on_lineEdit_textChanged(self, text):
        search = QtCore.QRegExp(    text,
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )

        self.proxy.setFilterRegExp(search)

    @QtCore.pyqtSlot(int)
    def on_comboBox_currentIndexChanged(self, index):
        self.proxy.setFilterKeyColumn(index)


if __name__ == "__main__":
    import sys

    app  = QtGui.QApplication(sys.argv)
    main = myWindow()
    main.show()
    main.resize(400, 600)
    sys.exit(app.exec_())

To get required results, a popup menu is launched by clicking on the header, and populated with the unique values for that column. Once an item in the popup menu is selected, the value is passed to self.proxy.setFilterRegExp(filterString) and the column to self.proxy.setFilterKeyColumn(filterValue).

image

Solution 2

I tried to update the answer provided in the above for PyQt5

from PyQt5 import QtCore, QtGui, QtWidgets

class myWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)
        self.centralwidget  = QtWidgets.QWidget(self)
        self.lineEdit       = QtWidgets.QLineEdit(self.centralwidget)
        self.view           = QtWidgets.QTableView(self.centralwidget)
        self.comboBox       = QtWidgets.QComboBox(self.centralwidget)
        self.label          = QtWidgets.QLabel(self.centralwidget)

        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
        self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
        self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)

        self.setCentralWidget(self.centralwidget)
        self.label.setText("Regex Filter")

        self.model = QtGui.QStandardItemModel(self)

        for rowName in range(3*5):
            self.model.invisibleRootItem().appendRow(
                [   QtGui.QStandardItem("row {0} col {1}".format(rowName, column))    
                    for column in range(3)
                    ]
                )

        self.proxy = QtCore.QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)

        self.view.setModel(self.proxy)
        self.comboBox.addItems(["Column {0}".format(x) for x in range(self.model.columnCount())])

        self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
        self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)

        self.horizontalHeader = self.view.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)

    @QtCore.pyqtSlot(int)
    def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
        self.logicalIndex   = logicalIndex
        self.menuValues     = QtWidgets.QMenu(self)
        self.signalMapper   = QtCore.QSignalMapper(self)  

        self.comboBox.blockSignals(True)
        self.comboBox.setCurrentIndex(self.logicalIndex)
        self.comboBox.blockSignals(True)

        valuesUnique = [    self.model.item(row, self.logicalIndex).text()
                            for row in range(self.model.rowCount())
                            ]

        actionAll = QtWidgets.QAction("All", self)
        actionAll.triggered.connect(self.on_actionAll_triggered)
        self.menuValues.addAction(actionAll)
        self.menuValues.addSeparator()

        for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):              
            action = QtWidgets.QAction(actionName, self)
            self.signalMapper.setMapping(action, actionNumber)  
            action.triggered.connect(self.signalMapper.map)  
            self.menuValues.addAction(action)

        self.signalMapper.mapped.connect(self.on_signalMapper_mapped)  

        headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())        

        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)

        self.menuValues.exec_(QtCore.QPoint(posX, posY))

    @QtCore.pyqtSlot()
    def on_actionAll_triggered(self):
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  "",
                                        QtCore.Qt.CaseInsensitive,
                                        QtCore.QRegExp.RegExp
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(int)
    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        filterColumn = self.logicalIndex
        filterString = QtCore.QRegExp(  stringAction,
                                        QtCore.Qt.CaseSensitive,
                                        QtCore.QRegExp.FixedString
                                        )

        self.proxy.setFilterRegExp(filterString)
        self.proxy.setFilterKeyColumn(filterColumn)

    @QtCore.pyqtSlot(str)
    def on_lineEdit_textChanged(self, text):
        search = QtCore.QRegExp(    text,
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )

        self.proxy.setFilterRegExp(search)

    @QtCore.pyqtSlot(int)
    def on_comboBox_currentIndexChanged(self, index):
        self.proxy.setFilterKeyColumn(index)


if __name__ == "__main__":
    import sys

    app  = QtWidgets.QApplication(sys.argv)
    main = myWindow()
    main.show()
    main.resize(400, 600)
    sys.exit(app.exec_())

Solution 3

As per the answer from @user1006989 & @Behzad Jamali: The menu position for filtering on header won't popup at the exact position if the table has more columns than the current view port.

To have a correct positioning of the popup menu, use this line

posX = headerPos.x() + self.horizontalHeader.sectionViewportPosition(index)
Share:
28,579
Rao
Author by

Rao

Updated on July 19, 2020

Comments

  • Rao
    Rao almost 4 years

    I am using QTableView to display data retrieved from QtSql.QSqlQuery

    I want to know how can i create filters for it like in excel.

    enter image description here

    In the above image i need to get the filters for All heders (Sh_Code,SH_Seq,Stage) The filters will have unique values in of that column on which we can filter.

    Required result

    I need the Table view header with a dropbox listing all unique values in that column just like in excel below. No need of Top,Standard filter... as shown in image. Need only "All" and the unique "column items"

    enter image description here

    This is from my .NET application, uploaded for more clarity

    enter image description here

  • Rao
    Rao over 11 years
    Its not getting any thing filtered when i type "row 0 col 0" in filter box. i added print statements in on_lineEdit_textChanged on_comboBox_currentIndexChanged but they are never executed. I am working on Python 2.6.4
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao Forgot to connect the slots! I updated the code, it should be working now
  • Rao
    Rao over 11 years
    it worked by connecting the signals self.comboBox.currentIndexChanged.connect(self.on_comboBox_c‌​urrentIndexChanged) and self.lineEdit.textChanged.connect(self.on_lineEdit_textChang‌​ed) But i need a filters to be displayed on column headers, from where i can select it (for example like in excel). i have updated the the question with a excel snapshot which is required.
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao Checkout my updated answer, I added some information on how to display a filter popup menu on clicking the header
  • Rao
    Rao over 11 years
    can you edit your code for waht you suggested. I couldn't able to find the header click event.
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao The signal you are looking for is sectionClicked ( int logicalIndex )
  • Rao
    Rao over 11 years
    i tried self.view.horizontalHeader.sectionClicked (self.headerclick) but i get error AttributeError: 'builtin_function_or_method' object has no attribute 'sectionClicked'
  • Rao
    Rao over 11 years
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao Try this instead self.view.horizontalHeader().sectionClicked (self.headerclick)
  • Rao
    Rao over 11 years
    this self.view.horizontalHeader().sectionClicked.connect(self.hea‌​derclick) worked but how to get the column index from it....?
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao sectionClicked returns a number, which is the number of the column, please create a new post for any further questions to avoid extending this thread
  • Rao
    Rao over 11 years
    questions i am asking about this post only... Your answer is not fully completed. so i am getting doubts. For this reason only i asked you to update your answer as per your suggestion. as i am noob at QTableview
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao Note that my answer covers fully your original question and that I shouldn't have edited my answer to comply your new requirements, a new post should have been created instead, something like How to sort in QTableView like in excel. I will update my code to reflect the changes in your question, although you should be capable of writing the code you need by your own with the pointers that I gave you, please don't expect to be spoonfed and consider this matters in your future questions at SO
  • Admin
    Admin over 11 years
    @PBLNarasimhaRao Checkout my updated answer, I edited the code to enable sorting in QTableView like in Excel
  • Rao
    Rao over 11 years
    thx for the help. i will start a new post for my further doubts. As of now if i filter column 1 with row 0 col 0 and try to filter column 2, it should show only the visible unique values of column 2 (probably it should show row 0 col 1 only) but now its showing all the elements of column 2 (row 0 col 1, row 1 col 1, row 2 col 1)
  • Tuhin Mitra
    Tuhin Mitra over 2 years
    Good work thanks! This worked smooth
  • Tuhin Mitra
    Tuhin Mitra over 2 years
    Thanks for the effort.
  • Alana
    Alana over 2 years
    @Behzad Jamali from doc.qt.io/archives/qt-5.10/qsignalmapper.html which said that QSignalMapper Class is deprecated. I would like to know if there is any alternatives. Is action.triggered.connect(lambda: self.on_signalMapper_mapped(actionName)) a possible way to do so. Thank you.