2

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
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
hantic
  • 45
  • 5
  • Okay, now I understand you. You want everything up and then to the left. although I see it a bit complex – eyllanesc Apr 24 '19 at 21:32
  • If it's easier it could also work such that the number of columns is always consistent if there are at least that many matches. For example in the last case, you would have an a in every column for the top row and 2 in the second row. – hantic Apr 24 '19 at 22:00
  • The problem is not the number of columns or rows, the problem is the displacement of the items. – eyllanesc Apr 24 '19 at 22:03
  • Now have you changed the ordering criteria? Any other change? – eyllanesc Apr 25 '19 at 02:08
  • Sorry, I was reading it and realized I made an error. What you show looks perfect, will try it out! – hantic Apr 25 '19 at 15:32

1 Answers1

0

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Doesn't returning true for every row make it filter/hide every row? I tried implementing this, but everything gets hidden. – hantic Apr 24 '19 at 15:48
  • Sorry, I think there is some confusion. I want filtering to happen per item, so checking every row and every column. The issue I ran in to is if an item in the first row matches my string, and an item in the second row, and second column matches, the item in the second row, first column automatically will also show up. – hantic Apr 24 '19 at 18:34
  • Is it possible to reorder items in the proxy model such that an item that doesn't match can be moved to a column that doesn't have any matches as a work around? – hantic Apr 24 '19 at 18:40
  • I tried that, However it added an additional complication of when a list model doesn't have any matches, it is still taking up space and pushing out the matches. I just posted an additional question of how to query number of matches which would allow the use of tying in to zero matches hiding the whole list. https://stackoverflow.com/questions/55836727/how-can-you-get-the-list-of-items-that-match-a-qsortfilterproxyfilter – hantic Apr 24 '19 at 18:52
  • When I run your update everything displays as lists instead of as a table. I did try using multiple QListViews to build up a table and that worked for the filtering. – hantic Apr 24 '19 at 19:37
  • What I want is if just a single item (0,0) matches in a 3x3 table only (0,0) would be shown. If (0,0) and (0,1) match, then just those show up. Not the whole row or the whole column. – hantic Apr 24 '19 at 19:42
  • They would move to be displayed in a single row if the two matches were from different columns. – hantic Apr 24 '19 at 21:12
  • Updated with desired results. – hantic Apr 24 '19 at 21:27
  • I tested this out, and typing a single "a" worked great, however when typing "aa" It didn't match anything, and when I typed "b" it didn't match anything. Going to try and get this to work and will post any changes I had to make. – hantic Apr 25 '19 at 16:13
  • @hantic Python2 or Python3? – eyllanesc Apr 25 '19 at 18:29
  • I am using python 2 with pyside – hantic Apr 25 '19 at 18:57
  • @hantic The problem is that in that the division in python2 and python3 differ from what returns, I have already corrected it. Try it and tell me what you get – eyllanesc Apr 26 '19 at 02:13
  • Thank you so much! – hantic Apr 26 '19 at 15:04
  • Would you mind explaining why you switched List2TableProxyModel .reset_model to use a QTimer? – hantic Apr 26 '19 at 15:31