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)
Anand
  • 343
  • 1
  • 5
  • 18

2 Answers2

10

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Hey how to clear all column filters at a time ? is there any option like filter reset or clear filters?? @eyllanesc – Anand Nov 10 '17 at 06:01
  • 1
    It's the same as in the individual case, you have to pass to the columns that filter an empty string – eyllanesc Nov 10 '17 at 07:20
  • `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://stackoverflow.com/a/57845903/964478 – Alex P. Sep 08 '19 at 21:36
  • 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. – masky007 Oct 04 '21 at 11:58
  • @masky007 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. – eyllanesc Oct 04 '21 at 14:17
  • @eyllanesc 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. Thanks – masky007 Oct 04 '21 at 14:20
  • @masky007 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. – eyllanesc Oct 04 '21 at 14:20
  • @masky007 My solution is complete as it works in PyQt4 which is the goal of the OP. Bye. – eyllanesc Oct 04 '21 at 14:21
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)
Alex P.
  • 3,697
  • 9
  • 45
  • 110
  • 1
    Can you please provide an example, i am struggling on how to use this. Thank you – masky007 Oct 04 '21 at 10:29
  • 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))) – masky007 Oct 04 '21 at 12:06