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:
- QTreeView + my model with 1 column - works fine
- QTreeView + my model with 2 columns - works fine
- QTreeView + proxy model + my model with 1 column - works fine
- 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 callcurrentIndex = mdl->index(0, COLUMNS_COUNT-1, currentIndex);
the result is a null indexto solve change
currentIndex = mdl->index(0, COLUMNS_COUNT-1, currentIndex);
intocurrentIndex = 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. -
@michalos I'm not sure if I can help you but seems like model test code works for me.
What I did:
- Added modeltest.h and cpp into my project
- Added "testlib" into "QT" section of project file
- Added this to modeltest.h:
#include <QtTest/qtestcase.h> #define qVariantCanConvert(_type_, _var_) _var_.canConvert(QMetaType::_type_)
- 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