Problem with QTreeView, custom tree model and proxy model



  • Hello.
    I have rather hard problem. I need to implement my own tree data model (based on QAbstractItemModel) to use with QTreeView and QSortFilterProxyModel. I did it. It work almost ok but in some cases I can't use QTreeView's ability to select сurrent cell and scroll to cell. I don't know is it result of wrong model implementation or is it Qt bug.

    Test cases:

    1. QTreeView + my model with 1 column - works fine
    2. QTreeView + my model with 2 columns - works fine
    3. QTreeView + proxy model + my model with 1 column - works fine
    4. QTreeView + proxy model + my model with 2 columns - works only first few times

    Seems like proxy model can't handle my model properly. Can you please take a look at (or even try to run) sample code? Maybe you will see something that I can't see. Like wrong model implementation or something different.

    You can choose columns count with first macro option.
    You can choose to use or not to use proxy model with second macro option.
    You can test desired functionality (QTreeView::setCurrentIndex and QTreeView::scrollTo) with button.

    I tried to make example program as simple as possible so here it is (this is one single source file, may require to run qmake before compilation):

    // ---------------- Test options
    
    // Option 1: Choose 1 or 2
    #define COLUMNS_COUNT 2
    
    // Option 2: Comment or uncomment line below
    #define USE_PROXY
    
    // ---------------- Include
    
    #include <QAbstractItemModel>
    #ifdef USE_PROXY
    #include <QSortFilterProxyModel>
    #endif
    #include <QTreeView>
    #include <QPushButton>
    #include <QApplication>
    #include <QVBoxLayout>
    
    // ---------------- Model
    
    class CPersonAndFriends
    {
    public:
        QString     name;
        QStringList friends;
    };
    
    class CModel: public QAbstractItemModel
    {
        Q_OBJECT
    public:
        CModel(QObject* parent);
    
        virtual int         rowCount   (const QModelIndex& parentIndex = QModelIndex()) const;
        virtual int         columnCount(const QModelIndex& parentIndex = QModelIndex()) const;
        virtual QModelIndex index      (int row, int column, const QModelIndex& parentIndex = QModelIndex()) const;
        virtual QModelIndex parent     (const QModelIndex& itemIndex) const;
        virtual QVariant    data       (const QModelIndex& itemIndex, int role = Qt::DisplayRole) const;
    
        void appendPerson(QString personName);
        void appendFriend(QString friendName);
    
    private:
        QList<CPersonAndFriends> modelData;
    };
    
    CModel::CModel(QObject* parent): QAbstractItemModel(parent)
    {
    }
    
    int CModel::rowCount(const QModelIndex& parentIndex) const
    {
        if (!parentIndex.isValid())
            return modelData.size();
        else
        if(!parentIndex.parent().isValid())
            return modelData.at(parentIndex.row()).friends.size();
    
        return 0;
    }
    
    int CModel::columnCount(const QModelIndex&) const
    {
        return COLUMNS_COUNT;
    }
    
    QModelIndex CModel::index(int row, int column, const QModelIndex& parentIndex) const
    {
        if (!parentIndex.isValid())
            return createIndex(row, column, (void*)(0));
        else
            return createIndex(row, column, (void*)(parentIndex.row()+1));
    }
    
    QModelIndex CModel::parent(const QModelIndex& itemIndex) const
    {
        int row = (int)itemIndex.internalPointer();
    
        if (row == 0)
            return QModelIndex();
        else
            return index(row-1, 0);
    }
    
    QVariant CModel::data(const QModelIndex& itemIndex, int role) const
    {
        if (itemIndex.isValid() &&
            role == Qt::DisplayRole)
        {
            if (!itemIndex.parent().isValid())
                return modelData.at(itemIndex.row()).name;
            else if (!itemIndex.parent().parent().isValid())
                return modelData.at(itemIndex.parent().row()).friends.at(itemIndex.row());
        }
        return QVariant();
    }
    
    void CModel::appendPerson(QString personName)
    {
        int firstLast = modelData.size();
        CPersonAndFriends item;
        item.name = personName;
    
        beginInsertRows(QModelIndex(), firstLast, firstLast);
        modelData << item;
        endInsertRows();
    }
    
    void CModel::appendFriend(QString friendName)
    {
        if (modelData.size() == 0)
            return;
    
        int row = modelData.size()-1;
        CPersonAndFriends& item = modelData[row];
        QModelIndex parentIndex = index(row, 0);
        int firstLast = rowCount(parentIndex);
    
        beginInsertRows(parentIndex, firstLast, firstLast);
        item.friends << friendName;
        endInsertRows();
    }
    
    // ---------------- Window
    
    class CWidget: public QWidget
    {
        Q_OBJECT
    public:
        CWidget(QWidget* parent = 0);
    private:
    #ifdef USE_PROXY
        QSortFilterProxyModel* proxy;
    #endif
        QTreeView   view;
        CModel*     model;
        QPushButton button;
    private slots:
        void selectNextItem();
    };
    
    CWidget::CWidget(QWidget* parent): QWidget(parent)
    {
        setLayout(new QVBoxLayout);
        layout()->addWidget(&view);
        model = new CModel(this);
    #ifdef USE_PROXY
        proxy = new QSortFilterProxyModel(this);
        proxy->setSourceModel(model);
        view.setModel(proxy);
    #else
        view.setModel(model);
    #endif
    
        layout()->addWidget(&button);
        button.setText("Select next item");
        connect(&button, &QPushButton::clicked, this, &CWidget::selectNextItem);
    
        model->appendPerson("Homer");
        model->appendFriend("Moe");
        model->appendFriend("Barney");
        model->appendPerson("Philipe");
        model->appendFriend("Amy");
        model->appendFriend("Bender");
    }
    
    void CWidget::selectNextItem()
    {
    #ifdef USE_PROXY
        QAbstractItemModel* mdl = proxy;
    #else
        QAbstractItemModel* mdl = model;
    #endif
    
        QModelIndex currentIndex = view.selectionModel()->currentIndex();
    
        if (!currentIndex.isValid())
            currentIndex = mdl->index(0, COLUMNS_COUNT-1);
    
    #ifdef USE_PROXY
        if (currentIndex.model() == model)
            currentIndex = proxy->mapFromSource(currentIndex);
        Q_ASSERT(currentIndex.model() == proxy);
    #endif
    
        if (currentIndex.parent() == QModelIndex())
        {
            if (mdl->rowCount(currentIndex))
                currentIndex = mdl->index(0, COLUMNS_COUNT-1, currentIndex);
            else
            {
                int nextRow = currentIndex.row() + 1;
                if (nextRow >= mdl->rowCount())
                    nextRow = 0;
                currentIndex = mdl->index(nextRow, COLUMNS_COUNT-1);
            }
        }
        else
        {
            int nextRow = currentIndex.row() + 1;
            if (nextRow >= mdl->rowCount(currentIndex.parent()))
            {
                nextRow = currentIndex.parent().row() + 1;
                if (nextRow >= mdl->rowCount())
                    nextRow = 0;
                currentIndex = mdl->index(nextRow, COLUMNS_COUNT-1);
            }
            else
                currentIndex = mdl->index(nextRow, COLUMNS_COUNT-1, currentIndex.parent());
        }
    
    #ifdef USE_PROXY
        Q_ASSERT(currentIndex.model() == proxy);
    #endif
    
        // Commands below will not work for case of proxy model and 2 columns
    
        view.setCurrentIndex(currentIndex);
        view.scrollTo(currentIndex);
    }
    
    // ---------------- Main function
    
    #include "main.moc"
    
    int main( int argc, char *argv[] )
    {
        QApplication oApplication( argc, argv );
        CWidget oVMainWindow;
        oVMainWindow.show();
        return oApplication.exec();
    }
    
    

    It was tested with Qt 5.6.1 under windows.

    I tried to investigate it with no success. Internals of Qt are complicated, you know... All I found is QTreeView::scrollTo function tries to get viewIndex(index) and fails.

    // qtreeview:cpp - QTreeView::scrollTo
        int item = d->viewIndex(index);
        if (item < 0) // item is -1 in my case but index is valid
            return;
    

    UPD:
    possible related topic: http://stackoverflow.com/questions/17529654/qtreeviewscrollto-not-working
    another one: https://forum.qt.io/topic/58517/qtableview-scrollto-doesn-t-scroll-when-using-proxy
    But I check model of index so it is not my case.



  • No bug in Qt, just a bit of confusion with parents...
    The problem is in this snippet:

    if (nextRow >= mdl->rowCount(currentIndex.parent()))
            {
                nextRow = currentIndex.parent().row() + 1;
                if (nextRow >= mdl->rowCount())
                    nextRow = 0;
                currentIndex = mdl->index(nextRow, COLUMNS_COUNT-1);
            }
    

    when the first if is true you are setting the parent to index(nextRow,1,QModelIndex()) but this index has no children ( index(nextRow,0,QModelIndex()) is the one that has children) so when you then call currentIndex = mdl->index(0, COLUMNS_COUNT-1, currentIndex); the result is a null index

    to solve change currentIndex = mdl->index(0, COLUMNS_COUNT-1, currentIndex); into currentIndex = mdl->index(0, COLUMNS_COUNT-1, mdl->index(currentIndex.row(),0));

    P.S.
    When reimplementing QAbstractItemModel is always wise to make it run through the model test to make sure you did all the internals correctly.



  • Perfect! Big thanks for your time and solution.
    I feel like I was not attentive enough while inspecting my own code.



  • @VRonin Didn't know about model test. Seems like useful thing. Thanks.



  • Do You know where can I find source code for the model test, that works with Qt 5.7?
    Ive downloaded the official, but can't figure out how to build it.
    main problem is with: load(qttest_p4)



  • @michalos I'm not sure if I can help you but seems like model test code works for me.

    What I did:

    1. Added modeltest.h and cpp into my project
    2. Added "testlib" into "QT" section of project file
    3. Added this to modeltest.h:
    #include <QtTest/qtestcase.h>
    #define qVariantCanConvert(_type_, _var_) _var_.canConvert(QMetaType::_type_)
    
    1. Changed expressions like "qVariantCanConvert<QString> ( variant)" to "qVariantCanConvert(QString, variant)" in modeltest.h

    And... my sample model failed to pass the test of course :D



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