Search code examples
pythonpysideqtableviewqstandarditemmodelqsortfilterproxymodel

How to filter out individual items instead of just on a row and column basis?


I am trying to filter out items in a table based on string matching.

I have a QTableView displaying a Proxy Model to allow for filtering, however if an item in (0,0) and in (1,1) matches my string but item (1,0) doesn't, it will still be displayed.

For example:

from PySide.QtGui import *
from PySide.QtCore import *


class CustomProxyFilter(QSortFilterProxyModel):

    def __init__(self):
        super(CustomProxyFilter, self).__init__()

    def filterAcceptsColumn(self, source_column, parent):
        """Re-implementing built-in to hide columns with non matches."""
        model = self.sourceModel()
        matched_string = self.filterRegExp().pattern().lower()
        for row in range(model.rowCount()):
            item = model.item(row, source_column)
            if item and matched_string in model.item(row, source_column).text().lower():
                return True
        return False


class CustomTableView(QTableView):
    """Table view."""
    def __init__(self, line_edit):
        super(CustomTableView, self).__init__()

        custom_model = StandardTableModel()

        items = ["apple", "banana", "applebanana"]
        for i, item in enumerate(items):
            for v, second_item in enumerate(items):
                custom_model.setItem(i, v, QStandardItem(item))
        self.proxy_model = CustomProxyFilter()
        self.proxy_model.setSourceModel(custom_model)
        self.setModel(self.proxy_model)


        line_edit.textChanged.connect(self.proxy_model.setFilterRegExp)


class Window(QWidget):

    def __init__(self):
        super(Window, self).__init__()
        self.setLayout(QVBoxLayout())
        self.line_edit = QLineEdit()
        self.layout().addWidget(self.line_edit)
        self.layout().addWidget(CustomTableView(self.line_edit))

What I am hoping would happen is if my table looks like

a|b|c
-----
c|a|b

The resulting table after filtering by "a" would be

a|a

My current solve shows.

a|b
---
c|a

Update for additional cases

a|a|c
-----
a|x|b
-----
c|b|a

becomes

a|a|a
-----
a

This case

a|a|y|c
-------
a|a|w|a
-------
c|a|w|w

Becomes

a|a|a|a
-----
a|a|

Essentially each item would move towards the top left when able. When they are different names, they would arrange themselves in alphabetical order like this

1|2|3|4
-------
5|6|7|8

Solution

  • To implement what you require, I have implemented several connected proxies in the following way:

    model-->Table2ListProxyModel-->QSortFilterProxyModel-->List2TableProxyModel
    

    The idea is to convert it to the structure of a list since filtering a row is equivalent to filtering an item, the same proxy orders it, and then we convert the list to a table.

    import math
    from PySide import QtCore, QtGui
    
    
    class Table2ListProxyModel(QtGui.QSortFilterProxyModel):
        def columnCount(self, parent=QtCore.QModelIndex()):
            return 1
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if parent.isValid():
                return 0
            return self.sourceModel().rowCount() * self.sourceModel().columnCount()
    
        def mapFromSource(self, sourceIndex):
            if (
                sourceIndex.isValid()
                and sourceIndex.column() == 0
                and sourceIndex.row() < self.rowCount()
            ):
                r = sourceIndex.row()
                c = sourceIndex.column()
                row = c * sourceIndex.model().columnCount() + r
                return self.index(row, 0)
            return QtCore.QModelIndex()
    
        def mapToSource(self, proxyIndex):
            r = proxyIndex.row() / self.sourceModel().columnCount()
            c = proxyIndex.row() % self.sourceModel().columnCount()
            return self.sourceModel().index(r, c)
    
        def index(self, row, column, parent=QtCore.QModelIndex()):
            return self.createIndex(row, column)
    
    
    class List2TableProxyModel(QtGui.QSortFilterProxyModel):
        def __init__(self, columns=1, parent=None):
            super(List2TableProxyModel, self).__init__(parent)
            self._columns = columns
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            r = self.sourceModel().rowCount()
            if r < self._columns:
                return r
            return self._columns
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if parent.isValid():
                return 0
            row = math.ceil(self.sourceModel().rowCount()*1.0 / self._columns)
            return row
    
        def index(self, row, column, parent=QtCore.QModelIndex()):
            return self.createIndex(row, column)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            r = index.row()
            c = index.column()
            row = r * self.columnCount() + c
            if row < self.sourceModel().rowCount():
                return super(List2TableProxyModel, self).data(index, role)
    
        def mapFromSource(self, sourceIndex):
            r = math.ceil(sourceIndex.row()*1.0 / self.columnCount())
            c = sourceIndex.row() % self.columnCount()
            return self.index(r, c)
    
        def mapToSource(self, proxyIndex):
            if proxyIndex.isValid():
                r = proxyIndex.row()
                c = proxyIndex.column()
                row = r * self.columnCount() + c
                return self.sourceModel().index(row, 0)
            return QtCore.QModelIndex()
    
        def setSourceModel(self, model):
            model.rowsRemoved.connect(self.reset_model)
            model.rowsInserted.connect(self.reset_model)
            model.dataChanged.connect(self.reset_model)
            model.rowsMoved.connect(self.reset_model)
            model.layoutChanged.connect(self.reset_model)
            model.modelReset.connect(self.reset_model)
            super(List2TableProxyModel, self).setSourceModel(model)
    
        @QtCore.Slot()
        def reset_model(self):
            QtCore.QTimer.singleShot(0, self.invalidate)
    
        def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
            if role == QtCore.Qt.DisplayRole:
                return str(section)
            return super(List2TableProxyModel, self).headerData(
                section, orientation, role
            )
    
    
    class CustomTableView(QtGui.QTableView):
        def __init__(self, parent=None):
            super(CustomTableView, self).__init__(parent)
            custom_model = QtGui.QStandardItemModel()
            datas = (("ad", "cd", "ef"), ("ab", "ce", "eg"), ("aa", "cb", "eh"))
            for i, data in enumerate(datas):
                for v, text in enumerate(data):
                    custom_model.setItem(i, v, QtGui.QStandardItem(text))
            self.proxy_list = Table2ListProxyModel(self)
            self.proxy_list.setSourceModel(custom_model)
            self.proxy_sort_filter = QtGui.QSortFilterProxyModel(self)
            self.proxy_sort_filter.setSourceModel(self.proxy_list)
            self.proxy_table = List2TableProxyModel(
                columns=custom_model.columnCount(), parent=self
            )
            self.proxy_table.setSourceModel(self.proxy_sort_filter)
    
            self.setModel(self.proxy_table)
    
        @QtCore.Slot(str)
        def setFilter(self, text):
            self.proxy_sort_filter.setFilterWildcard(
                "*{}*".format(text) if text else ""
            )
            self.proxy_sort_filter.sort(0 if text else -1, QtCore.Qt.AscendingOrder)
    
    
    class Window(QtGui.QWidget):
        def __init__(self):
            super(Window, self).__init__()
    
            self.line_edit = QtGui.QLineEdit()
            self.tableview = CustomTableView()
    
            lay = QtGui.QVBoxLayout(self)
            lay.addWidget(self.line_edit)
            lay.addWidget(self.tableview)
            self.line_edit.textChanged.connect(self.tableview.setFilter)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtGui.QApplication(sys.argv)
        w = Window()
        w.show()
        sys.exit(app.exec_())
    

    enter image description here

    enter image description here

    enter image description here