[SOLVED] How to make visible / select first item using a (sub)string and QSortFilterProxyModel ?



  • Hello!

    I have a list box with QSortFilterProxyModel connected to it. It is used for sorting only, not for filtering. In another line edit control of the form I am entering a (sub)string. How can I make visible (and possibly select) first row that would stay visible if I used filtering? I.e. I know that if will call setFilterFixedString with entered string than only items containing that string will become visible. But I do not want to filter items. I want them all to stay in list box but find first that matches filtering criteria and make it visible in the list (possibly select all such items)

    Thanks you!



  • If all you want to do is select appropriate rows then I wouldn't do this in your proxy model as its not what they are designed to do. You can get the functionality you want easily enough by using your models match() method to find the appropriate indexes and then selecting these indexes via your views selection model. The following is a simple complete example:

    widget.h
    @
    #ifndef WIDGET_H
    #define WIDGET_H

    #include <QtGui>

    class Model : public QAbstractTableModel
    {
    Q_OBJECT
    public:
    Model(QWidget *parent = 0) : QAbstractTableModel(parent){}
    int columnCount(const QModelIndex &parent) const { return 1; }
    int rowCount(const QModelIndex &parent) const { return 100; }
    QVariant data(const QModelIndex &index, int role) const {
    if(!(index.isValid() && role==Qt::DisplayRole)) return QVariant();
    return QVariant(index.row());
    }
    };

    class List : public QListView
    {
    Q_OBJECT
    public:
    List(QWidget *parent=0) : QListView(parent){}

    public slots:
    void search(QString text){
    selectionModel()->clear();
    if(text.isEmpty()) return;

        QModelIndex start=model()->index(0,0);
        QModelIndexList indexes=model()->match(start, Qt::DisplayRole, text, -1);
        foreach(QModelIndex index, indexes) selectionModel()->select(index, QItemSelectionModel::Select);
    }
    

    };

    class Widget : public QWidget
    {
    Q_OBJECT

    public:
    Widget(QWidget *parent = 0);

    private:
    Model *model;
    List *list;
    QSortFilterProxyModel *proxy;
    QLineEdit *search;
    };

    #endif // WIDGET_H
    @

    widget.cpp
    @
    #include "widget.h"

    Widget::Widget(QWidget *parent)
    : QWidget(parent)
    {
    QVBoxLayout *l=new QVBoxLayout(this);
    search=new QLineEdit(this);
    search->setPlaceholderText("Search...");
    l->addWidget(search);

    model=new Model(this);
    proxy=new QSortFilterProxyModel(this);
    proxy->setSourceModel(model);
    list=new List(this);
    list->setModel(proxy);
    connect(search, SIGNAL(textChanged(QString)), list, SLOT(search(QString)));
    l->addWidget(list);
    

    }
    @

    main.cpp
    @
    #include <QtGui/QApplication>
    #include "widget.h"

    int main(int argc, char *argv[])
    {
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec&#40;&#41;;
    

    }
    @

    Model() is just a simple table model that returns a row number as data. The interesting part is the search() slot in the QListView() which is invoked by the QLineEdit()'s textChanged() signal. Typing '1' into the search box will cause all items beginning with '1' to be selected (1, 10-19). If you want to change the search behaviour for wildcarding or to use a regexp then just change the Qt::MatchFlags that are passed to the match method.

    Hope this helps ;o)



  • Thank you jazzycamel for you prompt and detailed answer. And sorry for my late respond ((

    I will try to implement what you suggested (although I need first to translate it to PyQt ))) )



  • I actually prototyped this in python first as follows:

    @
    import sip
    sip.setapi('QString', 2)
    sip.setapi('QVariant', 2)

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

    class Model(QAbstractTableModel):
    def rowCount(self , parent=QModelIndex()): return 100
    def columnCount(self, parent=QModelIndex()): return 1
    def data(self, index, role=Qt.DisplayRole):
    if not (index.isValid() and role==Qt.DisplayRole): return None
    return "{0}".format(index.row())

    class Proxy(QSortFilterProxyModel): pass
    class List(QListView):
    @pyqtSlot(str)
    def search(self, text):
    self.selectionModel().clear()
    if text=="": return

        start=self.model().index(0,0)
        indexes=self.model().match(start, Qt.DisplayRole, text, hits=-1)
    
        for index in indexes:
            self.selectionModel().select(index, QItemSelectionModel.Select)
    

    if name=="main":
    from sys import argv, exit

    class Widget(QWidget):
        def __init__(self, parent=None):
            QWidget.__init__(self, parent)
    
            l=QVBoxLayout(self)
    
            self._search=QLineEdit(self, placeholderText="Search...")
            l.addWidget(self._search)
    
            self._model=Model(self)
            self._proxy=Proxy(self)
            self._proxy.setSourceModel(self._model)
            self._list=List(self)
            self._list.setModel(self._proxy)
            self._search.textChanged.connect(self._list.search)
    
            l.addWidget(self._list)
    
            self.setFocus()
    
    a=QApplication(argv)
    w=Widget()
    w.show()
    w.raise_()
    exit(a.exec_())
    

    @

    Might save you a bit of time and effort. Hope this helps ;o)



  • Thank you again jazzycamel for your code snippets!

    First of all, in response to your first reply I want to say that that I used proxy model for the purpose it is designed - I want my ListView to be sorted. And that also helped me to filter items of course. My original code was:
    @
    def lineEditTextEdited(self, text):
    self.listModel.setFilterFixedString(text)
    @

    'self.listModel' in my widget is QSortFilterProxyModel, I'm saving it in setModel() method of widget:
    @
    def setModel(self, model):
    self.listModel = model
    self.listView.setModel(model)
    @

    Now I am trying to use your suggestion. If I understand your code correctly in line 44 you are setting your list's model to proxy model, so in lines 22 and 23 you are calling methods index and match of proxy model. So my first version after change was:

    @
    def lineEditTextEdited(self, text):
    #self.listModel.setFilterFixedString(text)
    self.listView.selectionModel().clear()
    if text=="": return
    start=self.listModel.index(0,0)
    indexes=self.listModel.match(start, QtCore.Qt.DisplayRole, text, hits=-1)
    for index in indexes:
    self.listView.selectionModel().select(index, QItemSelectionModel.Select)
    @

    It didn't work and since I understood from your first reply that I should use original model, not proxy the second version is:

    @
    def lineEditTextEdited(self, text):
    #self.listModel.setFilterFixedString(text)
    self.listView.selectionModel().clear()
    if text=="": return
    start=self.listModel.sourceModel().index(0,0)
    indexes=self.listModel.sourceModel().match(start, QtCore.Qt.DisplayRole, text, hits=-1)
    for index in indexes:
    self.listView.selectionModel().select(index, QItemSelectionModel.Select)
    @

    In both versions 'indexes' result is empty array (0 items)...

    Can you please tell me what is wrong in my code?



  • In line 22-23 of my example I am calling the model() method on my view which will return the model to which that view is attached, in this case its the proxy model so your first attempt was correct.

    I'm afraid I can't tell you why this isn't working for your particular application without seeing a bit more code. What type does the data() method of your model return for the DisplayRole? I'm wondering if match is failing because its comparing search type string to something else (QVariant for example).

    Sorry I can't be more help.



  • I think the problem is mathflags. If I am typing string from the beginning then everything is working as expected (I see that default value of 'flags' parameter of match method is Qt::MatchFlags( Qt::MatchStartsWith | Qt::MatchWrap )

    But I trying to match by substring - like setFilterFixedString(text) did. So I tried:

    @
    start=self.listModel.sourceModel().index(0,0)
    flags = Qt.MatchFlags(Qt.MatchFixedString)
    indexes=self.listModel.sourceModel().match(start, QtCore.Qt.DisplayRole, text, -1, flags)
    @

    and since you confirmed that I don't need source model:
    @
    start=self.listModel.index(0,0)
    flags = Qt.MatchFlags(Qt.MatchFixedString)
    indexes=self.listModel.match(start, QtCore.Qt.DisplayRole, text, -1, flags)
    @

    still indexes contains 0 items... and even worse - no matches even if type string from the begging..
    I suspect that I am not settings flags correctly...

    my data() method returns String for DisplayRole...



  • Yes, flags should be Qt.MatchFlags(Qt.MatchContains | Qt.MatchWrap)
    (method name 'setFilterFixedString' confused me...)

    now 'indexes' are exactly what I expect them to be

    but self.listView.selectionModel().select(index, QItemSelectionModel.Select) has no visual effect - no items actually selected...



  • I was just about to suggest MatchContains ;o)

    The indexes needs to belong to which ever model the view is attached to. If you are calling match() on the source model (which you shouldn't need to, the proxy will be fine as per example) then you will have to map them back to the proxy using mapFromSource().



  • Thank you again )))

    no, as I mentioned in previous post I removed sourceModel(). call from code

    'self.listModel' is what ListView is attached to

    something is wrong with my ListView itself (regardless to code being discussed) - It doesn't select rows even when I click on them...



  • Anyway, the original question is answered by jazzycamel

    How do I mark this topic [SOLVED]?


  • Lifetime Qt Champion

    Hi,

    Simply edit the thread title and add [solved] to it :)



  • Thank you SGaist! :)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.