How to filter Multiple column in Qtableview?
Asked Answered
B

3

8

I'm using QtableView to show my logs and to filter them by column, QSortFilterProxyModel is used. If i filter one column using certain value, and with the filtered data, if i try to filter second column, last filter gets reset and data are displayed corresponding to filter on second column. I want to achieve multiple column filter on Qtableview.

Code snippet:

self.tableView = QTableView()
self.model = QtGui.QStandardItemModel(self)
self.proxy = QtGui.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.tableView.setModel(self.proxy)

def updateTable(self):
    self.model.invisibleRootItem().appendRow(,,,,)

def filterTable(self, stringAction, filterColumn):
    filterString = QtCore.QRegExp(  stringAction,
                                    QtCore.Qt.CaseSensitive,
                                    QtCore.QRegExp.FixedString
                                    )

    self.proxy.setFilterRegExp(filterString)
    self.proxy.setFilterKeyColumn(filterColumn)
Bora answered 9/11, 2017 at 12:11 Comment(0)
S
11

You must create a class that inherits from QSortFilterProxyModel, and overwrite the filterAcceptsRow method where False is returned if at least one item is not satisfied and True if all are satisfied.

class SortFilterProxyModel(QSortFilterProxyModel):
    def __init__(self, *args, **kwargs):
        QSortFilterProxyModel.__init__(self, *args, **kwargs)
        self.filters = {}

    def setFilterByColumn(self, regex, column):
        self.filters[column] = regex
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row, source_parent):
        for key, regex in self.filters.items():
            ix = self.sourceModel().index(source_row, key, source_parent)
            if ix.isValid():
                text = self.sourceModel().data(ix).toString()
                if not text.contains(regex):
                    return False
        return True

Example:

def random_word():
    letters = "abcdedfg"
    word = "".join([choice(letters) for _ in range(randint(4, 7))])
    return word


class Widget(QWidget):
    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        self.setLayout(QVBoxLayout())

        tv1 = QTableView(self)
        tv2 = QTableView(self)
        model = QStandardItemModel(8, 4, self)
        proxy = SortFilterProxyModel(self)
        proxy.setSourceModel(model)
        tv1.setModel(model)
        tv2.setModel(proxy)
        self.layout().addWidget(tv1)
        self.layout().addWidget(tv2)

        for i in range(model.rowCount()):
            for j in range(model.columnCount()):
                item = QStandardItem()
                item.setData(random_word(), Qt.DisplayRole)
                model.setItem(i, j, item)

        flayout = QFormLayout()
        self.layout().addLayout(flayout)
        for i in range(model.columnCount()):
            le = QLineEdit(self)
            flayout.addRow("column: {}".format(i), le)
            le.textChanged.connect(lambda text, col=i:
                                   proxy.setFilterByColumn(QRegExp(text, Qt.CaseSensitive, QRegExp.FixedString),
                                                           col))


if __name__ == '__main__':
    import sys

    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())
Scrappy answered 9/11, 2017 at 14:9 Comment(8)
Hey how to clear all column filters at a time ? is there any option like filter reset or clear filters?? @ScrappyBora
It's the same as in the individual case, you have to pass to the columns that filter an empty stringScrappy
text.contains(regex) looks suspicious, are you sure that it works correctly (unless regex is just string, not QRegExp)? btw here is my a bit more advanced version https://mcmap.net/q/1275491/-how-to-filter-multiple-column-in-qtableviewSaurischian
Your solution is not fully working. "contains" is not a thing in Python, no? Also you can't just check if text in regex (equivalent in Python) since regex is PySide2.QtCore.QRegExp object and it's not iterable - please modify your answer as it's not complete.Etz
@Etz If my solution does not match your requirement or you think it is not correct then give it a DV and post your correct answer.Scrappy
@Scrappy i think it was enough to point out what's missing, your answer is almost fully complete so you can just modify that part of the code to be up to date and fully working in Python. ThanksEtz
@Etz Note: My solution is based on the information provided by the OP, in this case the technology used was PyQt4, not PySide2. PyQtX and PySideY may have differences so the solution may work on one and fail on the other. I answer what the OP requested.Scrappy
@Etz My solution is complete as it works in PyQt4 which is the goal of the OP. Bye.Scrappy
S
4

Here is a bit more advanced implementation, it has more convenient clearing and supports different column combination modes: AND (the same as the original, all specified columns must match), OR.

PySide2, Python 3.

import re

from PySide2.QtCore import Qt, QSortFilterProxyModel


class MultiFilterMode:
    AND = 0
    OR = 1


class MultiFilterProxyModel(QSortFilterProxyModel):    
    def __init__(self, *args, **kwargs):
        QSortFilterProxyModel.__init__(self, *args, **kwargs)
        self.filters = {}
        self.multi_filter_mode = MultiFilterMode.AND

    def setFilterByColumn(self, column, regex):
        if isinstance(regex, str):
            regex = re.compile(regex)
        self.filters[column] = regex
        self.invalidateFilter()

    def clearFilter(self, column):
        del self.filters[column]
        self.invalidateFilter()

    def clearFilters(self):
        self.filters = {}
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row, source_parent):
        if not self.filters:
            return True

        results = []
        for key, regex in self.filters.items():
            text = ''
            index = self.sourceModel().index(source_row, key, source_parent)
            if index.isValid():
                text = self.sourceModel().data(index, Qt.DisplayRole)
                if text is None:
                    text = ''
            results.append(regex.match(text))

        if self.multi_filter_mode == MultiFilterMode.OR:
            return any(results)
        return all(results)
Saurischian answered 8/9, 2019 at 21:32 Comment(2)
Can you please provide an example, i am struggling on how to use this. Thank youEtz
I am getting, AttributeError: 'PySide2.QtCore.QRegExp' object has no attribute 'match' when i try to call the setFilterByColumn like this: self.lineEdit.textChanged.connect(lambda text, col=i: self.proxy.setFilterByColumn(col, QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)))Etz
A
0

the answer does not work in the PyQt5. If you rewrite the code as follows, then everything is ok

class SortFilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, *args, **kwargs):
        QtCore.QSortFilterProxyModel.__init__(self, *args, **kwargs)
        self.filters = {}

    def setFilterByColumn(self, regex, column):
        self.filters[column] = regex
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row, source_parent):
        for key, regex in self.filters.items():
            ix = self.sourceModel().index(source_row, key, source_parent)
            if ix.isValid():
                text = self.sourceModel().data(ix)
                #if not text.contains(regex):
                if regex.indexIn(text, 0)==-1:
                    return False
        return True
Amagasaki answered 22/11, 2023 at 20:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.