18

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
  • 2,902
  • 14
  • 52
  • 70

4 Answers4

32

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

  • 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 – Rao Jan 02 '13 at 07:39
  • @PBLNarasimhaRao Forgot to connect the slots! I updated [the code](http://stackoverflow.com/a/14075797/1006989), it should be working now –  Jan 02 '13 at 07:47
  • it worked by connecting the signals `self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)` and `self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)` 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. – Rao Jan 02 '13 at 07:48
  • @PBLNarasimhaRao Checkout my [updated answer](http://stackoverflow.com/a/14075797/1006989), I added some information on how to display a filter popup menu on clicking the header –  Jan 02 '13 at 08:15
  • can you edit your code for waht you suggested. I couldn't able to find the header click event. – Rao Jan 02 '13 at 08:30
  • @PBLNarasimhaRao The signal you are looking for is [`sectionClicked ( int logicalIndex )`](http://doc.qt.digia.com/4.7-snapshot/qheaderview.html#sectionClicked) –  Jan 02 '13 at 08:41
  • i tried `self.view.horizontalHeader.sectionClicked (self.headerclick)` but i get error `AttributeError: 'builtin_function_or_method' object has no attribute 'sectionClicked'` – Rao Jan 02 '13 at 08:50
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/22034/discussion-between-pbl-narasimha-rao-and-x-jacobs) – Rao Jan 02 '13 at 08:54
  • @PBLNarasimhaRao Try this instead `self.view.horizontalHeader().sectionClicked (self.headerclick)` –  Jan 02 '13 at 08:54
  • this `self.view.horizontalHeader().sectionClicked.connect(self.headerclick)` worked but how to get the column index from it....? – Rao Jan 02 '13 at 09:15
  • @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 –  Jan 02 '13 at 09:27
  • 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 – Rao Jan 02 '13 at 10:06
  • @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 –  Jan 02 '13 at 11:07
  • @PBLNarasimhaRao Checkout my [updated answer](http://stackoverflow.com/a/14075797/1006989), I edited the code to enable sorting in `QTableView` like in Excel –  Jan 02 '13 at 13:57
  • 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`) – Rao Jan 03 '13 at 04:50
10

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_())
Ansh David
  • 654
  • 1
  • 10
  • 26
Behzad Jamali
  • 884
  • 2
  • 10
  • 23
  • @Behzad Jamali from https://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. – Alana Feb 18 '22 at 15:02
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)
Erich
  • 1,838
  • 16
  • 20
hbkvikas
  • 69
  • 4
0

I tried to update the answer for PyQt6. I used QListWidget instead of QMenu, it will creater a scroll bar when you got a lot of data. I create a funtion to read the excel file. For the position issue of filter, I add a judgment statement, so that the filter won't go outside the screen.

import sys
from PyQt6.QtWidgets import *

from PyQt6 import QtWidgets
from PyQt6 import QtCore,QtGui
import pandas as pd

class Table(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    def initUI(self):
        self.setWindowTitle('QTableWidget')
        self.setGeometry(300, 300, 500, 300)
        self.table = QTableWidget()
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
        self.centralwidget = QtWidgets.QWidget(self)

        self.btn1 = QPushButton('Unfilter', self)
        self.btn1.clicked.connect(self.Unfilter)

        # 创建布局
        self.filter_layout = QHBoxLayout()
        self.filter_layout.addStretch(3)
        self.filter_layout.addWidget(self.btn1)

        self.layout = QVBoxLayout()
        self.layout.addLayout(self.filter_layout)
        self.layout.addWidget(self.table)
        self.setLayout(self.layout)

        self.horizontalHeader = self.table.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
        filename = 'test.xlsx'
        self.df1 = pd.read_excel(filename, engine='openpyxl', sheet_name='Sheet1', header=0)
        # self.df1['Date'] =self.df1['Date'].dt.date
        self.insertData(self.df1)

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

        valuesUnique = [self.table.item(row, self.logicalIndex).text() for row in range(self.table.rowCount()) if not self.table.isRowHidden(row)]

        # 子窗口
        self.Menudialog = QDialog(self)
        self.Menudialog.setWindowTitle('Sub Window')
        self.Menudialog.resize(200, 150)

        self.list_widget = QListWidget(self)
        self.list_widget.addItems(sorted(list(set(valuesUnique))))
        # create QScrollArea,put QListWidget in it
        scroll_area = QScrollArea(self.Menudialog)
        scroll_area.setWidgetResizable(True)
        scroll_area.setWidget(self.list_widget)

        self.list_widget.currentItemChanged.connect(self.on_list_item_clicked)
        layout = QVBoxLayout(self.Menudialog)
        layout.addWidget(scroll_area)

        self.setLayout(layout)

        headerPos = self.table.mapToGlobal(self.horizontalHeader.pos())
        posY = headerPos.y() + self.horizontalHeader.height()
        # posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)
        posX = headerPos.x() + self.horizontalHeader.sectionViewportPosition(self.logicalIndex)
        print(posX)
        if posX > 1700:
            posX = 1620

        self.Menudialog.setGeometry(posX +100, posY, 200, 300)
        self.Menudialog.exec()

        ''' 
        actionAll = QtGui.QAction("All", self)
        actionAll.triggered.connect(self.Unfilter)
        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.mappedInt.connect(self.on_signalMapper_mapped)

        headerPos = self.table.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))
        '''
    def on_list_item_clicked(self):
        item = self.list_widget.currentItem()
        column = self.logicalIndex
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != item.text():
                self.table.setRowHidden(i, True)
        self.Menudialog.close()

    def on_signalMapper_mapped(self, i):
        stringAction = self.signalMapper.mapping(i).text()
        column = self.logicalIndex
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != stringAction:
                self.table.setRowHidden(i, True)

    def filter(self):
        column = self.filter_box.currentIndex()
        for i in range(self.table.rowCount()):
            if self.table.item(i, column).text() != self.search_box.text() and self.search_box.text() !='':
                self.table.setRowHidden(i, True)

    def search(self):
        text = self.search_box.text()
        for i in range(self.table.rowCount()):
            self.table.setRowHidden(i, False)

        for i in range(self.table.rowCount()):
            for j in range(self.table.columnCount()):
                if text in self.table.item(i, j).text():
                    break
                else:
                    self.table.setRowHidden(i, True)

    def Unfilter(self):
        for i in range(self.table.rowCount()):
                self.table.showRow(i)
    def insertData(self, df1):
        if len(df1) < 1:
            msgBox = QMessageBox(self)
            msgBox.setWindowTitle('注意')
            msgBox.setText('数据集为空,不能显示!')
            msgBox.setStandardButtons(QMessageBox.StandardButton.Yes)
            msgBox.exec()
            return
        # 获取列名
        self.header = df1.columns.tolist()
        # 获取数据
        data = df1.values.tolist()
        self.row =len(data)
        self.col = len(data[0])
        # 设置表格的行数为0,以清空之前的内容
        self.table.setRowCount(0)
        self.table.setRowCount(self.row)
        self.table.setColumnCount(self.col)
        self.table.setHorizontalHeaderLabels(self.header)
        if data:
            # 按行添加数据
            for r in range(self.row):
                for c in range(self.col):
                    # 添加表格数据
                    newItem = QTableWidgetItem(str(data[r][c]))
                    self.table.setItem(r, c, newItem)
        else:
            self.label.setText('无数据')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    table = Table()
    table.show()
    sys.exit(app.exec())
flyfairy
  • 1
  • 1