alphanumeric
alphanumeric

Reputation: 19329

How to select QTableView index or row from inside of Model

The posted code creates a singe Model/Proxy QTableView. The multi-selection feature has been enabled for it.

enter image description here

There are four items total. Two of them include a character "A". Other two include character "B" in their "item" names.

QPushButton when pressed calls for the clicked() method. When called this method first queries a Proxy Model connected to the QTableView:

proxyModel=self.tableview.model()

Then the method asks a proxyModel to return a total number of rows:

rows=proxyModel.rowCount()

Knowing how many rows in a QTabelView's model it iterates each row. First it is querying a row index:

index=proxyModel.index(row, 0)

Knowing index it proceeds with asking for a value stored in self.items variable by calling data() method supplying it with a queried in a previous step a QModelIndex (a variable index here) and a Role flag.

item=proxyModel.data(index, Qt.DisplayRole).toPyObject()

'toPyObject()' is used to convert the data received from .data() method to a "regular" Python variable.

Lastly it checks if the characters "B" in a received string. If so it selects QTableView row using:

self.tableview.selectRow(row)

Now what I want is to get the same selection functionality from inside of the scope of Proxy Model's filterAcceptsRow() if that is possible.

If it is not possible I would like to know if there is any other way of doing it... should be I using QItemSelectionModel? Then how?

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys

class Model(QAbstractTableModel):
    def __init__(self, parent=None, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.items = ['Item_A_001','Item_A_002','Item_B_001','Item_B_002']

    def rowCount(self, parent=QModelIndex()):
        return len(self.items)       
    def columnCount(self, parent=QModelIndex()):
        return 1

    def data(self, index, role):
        if not index.isValid(): return QVariant()
        elif role != Qt.DisplayRole:
            return QVariant()

        row=index.row()
        if row<len(self.items):
            return QVariant(self.items[row])
        else:
            return QVariant()

class Proxy(QSortFilterProxyModel):
    def __init__(self):
        super(Proxy, self).__init__()

    def filterAcceptsRow(self, row, parent):
        return True

class MyWindow(QWidget):
    def __init__(self, *args):
        QWidget.__init__(self, *args)

        tableModel=Model(self)               

        proxyModel=Proxy()
        proxyModel.setSourceModel(tableModel)

        self.tableview=QTableView(self) 
        self.tableview.setModel(proxyModel)
        self.tableview.horizontalHeader().setStretchLastSection(True)
        self.tableview.setSelectionMode(QAbstractItemView.MultiSelection)

        button=QPushButton(self)
        button.setText('Select Items with B')
        button.clicked.connect(self.clicked)

        layout = QVBoxLayout(self)
        layout.addWidget(self.tableview)
        layout.addWidget(button)
        self.setLayout(layout)

    def clicked(self, arg):
        proxyModel=self.tableview.model()

        self.tableview.clearSelection()
        rows=proxyModel.rowCount()
        for row in range(rows):
            index=proxyModel.index(row, 0)
            item=proxyModel.data(index, Qt.DisplayRole).toPyObject()
            if '_B_' in item:
                self.tableview.selectRow(row)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

Upvotes: 1

Views: 6329

Answers (1)

nb1987
nb1987

Reputation: 1410

You can achieve the selection within the filterAcceptsRow() method of the proxy model, but doing so would require the following:

  1. That your proxy model (or source model) contain a reference to the QTableView instance.
  2. That your proxy model contain an attribute indicating whether it is active. This is because you want to only select the table rows when the button is clicked, but filterAcceptsRow() is called automatically by the proxy model. Therefore, you would want to avoid calling the view's selectRow() method until the button is clicked.

To achieve #1, you could define a simple setter method in your proxy model class:

def setView(self, view):
    self._view = view

You would also need to of course invoke that setter within your MyWindow class's constructor:

proxyModel.setView(self.tableview)

Achieving #2 is a simple matter of creating this attribute in the proxy model class's constructor

self.filterActive = False

Now that your classes are prepared, you can implement your desired behavior. In your filterAcceptsRow() re-implementation, you only want to select the rows if they contain '_B_' and is the filter is active (that is, the button was clicked):

def filterAcceptsRow(self, row, parent):
    if self.filterActive and '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
        self._view.selectRow(row)
    return True

Finally, you want to make sure that these conditions are met once the button is clicked, so in your clicked() method you need to set the proxyModel's filterActive attribute to True and you need to call the QSortFilterProxyModel class's invalidateFilter() method to indicate that the existing filter is invalid and therefore filterAcceptsRow() should be called again:

def clicked(self, arg):
    proxyModel=self.tableview.model()
    self.tableview.clearSelection()
    proxyModel.filterActive = True
    proxyModel.invalidateFilter()

So the new code, in full, is:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys

class Model(QAbstractTableModel):
    def __init__(self, parent=None, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.items = ['Item_A_001','Item_A_002','Item_B_001','Item_B_002']

    def rowCount(self, parent=QModelIndex()):
        return len(self.items)       
    def columnCount(self, parent=QModelIndex()):
        return 1

    def data(self, index, role):
        if not index.isValid(): return QVariant()
        elif role != Qt.DisplayRole:
            return QVariant()

        row=index.row()
        if row<len(self.items):
            return QVariant(self.items[row])
        else:
            return QVariant()

class Proxy(QSortFilterProxyModel):
    def __init__(self):
        super(Proxy, self).__init__()
        self.filterActive = False

    def setView(self, view):
        self._view = view

    def filterAcceptsRow(self, row, parent):
        if self.filterActive and '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
            self._view.selectRow(row)
        return True

class MyWindow(QWidget):
    def __init__(self, *args):
        QWidget.__init__(self, *args)

        tableModel=Model(self)               

        proxyModel=Proxy()
        proxyModel.setSourceModel(tableModel)

        self.tableview=QTableView(self) 
        self.tableview.setModel(proxyModel)
        self.tableview.horizontalHeader().setStretchLastSection(True)
        self.tableview.setSelectionMode(QAbstractItemView.MultiSelection)

        proxyModel.setView(self.tableview)

        button=QPushButton(self)
        button.setText('Select Items with B')
        button.clicked.connect(self.clicked)

        layout = QVBoxLayout(self)
        layout.addWidget(self.tableview)
        layout.addWidget(button)
        self.setLayout(layout)

    def clicked(self, arg):
        proxyModel=self.tableview.model()
        self.tableview.clearSelection()
        proxyModel.filterActive = True
        proxyModel.invalidateFilter()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MyWindow()
    w.show()
    sys.exit(app.exec_())

Having said all of that, the purpose of filterAcceptsRow() is so that you can implement your own custom filtering in a subclass of QSortFilterProxyModel. So, a more typical implementation (following your desired rule) would be:

def filterAcceptsRow(self, row, parent):
    if not self.filterActive or '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
        return True
    return False

And even then, because the filtering could be done with regex, reimplementation of filterAcceptsRow() isn't even necessary. You could just call proxyModel.setFilterRegExp(QRegExp("_B_", Qt.CaseInsensitive, QRegExp.FixedString)) and proxyModel.setFilterKeyColumn(0) to achieve the same thing, filter-wise.

Hope that helps!

Upvotes: 3

Related Questions