23

What I have:

  1. QTreeView class with table data
  2. And connected QAbstractTableModel model

Question: how to save expanded state of items? Is some one have finished solutions?

PS: I know, that I can do this code by myself, but I don't have much time, and this is not the major problem of our project, but still we need it, because app contain a lot of such tables, and every time expanding tree items is annoyed process...

Bill the Lizard
  • 398,270
  • 210
  • 566
  • 880
mosg
  • 12,041
  • 12
  • 65
  • 87
  • Can you expand your requirements a bit? Do you mean preserve the expanded states across program executions, by perhaps storing the data in QSettings? Or preserving the expand state when modifying the tree? – Casey Jul 15 '10 at 18:14
  • @Casey Yes, store `QByteArray` with QSettings, first. No modifications, second. Here: http://www.qtcentre.org/threads/13826-QTreeView-restore-Expanded-node-after-reload-model I found some realization, but have no time to check... – mosg Jul 16 '10 at 06:20
  • > No modifications - means, I would like to restore last previous items expands when my application starts. – mosg Jul 16 '10 at 06:27

8 Answers8

17

First, thanks to Razi for persistentIndexList and isExpanded way.

Second, here is the code which works for me just fine :-)

dialog.h file:

class Dialog : public QDialog
{
    Q_OBJECT;

    TreeModel *model;
    TreeView *view;

public:
    Dialog(QWidget *parent = 0);
    ~Dialog(void);

    void reload(void);

protected:
    void createGUI(void);
    void closeEvent(QCloseEvent *);
    void saveState(void);
    void restoreState(void);
};

dialog.cpp file:

Dialog::Dialog(QWidget *parent)
{
    createGUI();
    reload();
}

Dialog::~Dialog(void) {};

void Dialog::reload(void)
{
    restoreState();
}

void Dialog::createGUI(void)
{
    QFile file(":/Resources/default.txt");
    file.open(QIODevice::ReadOnly);
    model = new TreeModel(file.readAll());
    file.close();

    view = new TreeView(this);
    view->setModel(model);

    QVBoxLayout *mainVLayout = new QVBoxLayout;
    mainVLayout->addWidget(view);

    setLayout(mainVLayout);
}

void Dialog::closeEvent(QCloseEvent *event_)
{
    saveState();
}

void Dialog::saveState(void)
{
    QStringList List;

    // prepare list
    // PS: getPersistentIndexList() function is a simple `return this->persistentIndexList()` from TreeModel model class
    foreach (QModelIndex index, model->getPersistentIndexList())
    {
        if (view->isExpanded(index))
        {
            List << index.data(Qt::DisplayRole).toString();
        }
    }

    // save list
    QSettings settings("settings.ini", QSettings::IniFormat);
    settings.beginGroup("MainWindow");
    settings.setValue("ExpandedItems", QVariant::fromValue(List));
    settings.endGroup();
}

void Dialog::restoreState(void)
{
    QStringList List;

    // get list
    QSettings settings("settings.ini", QSettings::IniFormat);
    settings.beginGroup("MainWindow");
    List = settings.value("ExpandedItems").toStringList();
    settings.endGroup();

    foreach (QString item, List)
    {
        // search `item` text in model
        QModelIndexList Items = model->match(model->index(0, 0), Qt::DisplayRole, QVariant::fromValue(item));
        if (!Items.isEmpty())
        {
            // Information: with this code, expands ONLY first level in QTreeView
            view->setExpanded(Items.first(), true);
        }
    }
}

Have a nice day!)


PS: this example based on C:\Qt\4.6.3\examples\itemviews\simpletreemodel code.

mosg
  • 12,041
  • 12
  • 65
  • 87
8

Thanks to Razi and mosg I was able to get this working. I made it restore the expanded state recursively so I thought I would share that part.

void applyExpandState_sub(QStringList& expandedItems,
                          QTreeView* treeView,
                          QAbstractItemModel* model,
                          QModelIndex startIndex)
{
    foreach (QString item, expandedItems) 
    {
        QModelIndexList matches = model->match( startIndex, Qt::UserRole, item );
        foreach (QModelIndex index, matches) 
        {
            treeView->setExpanded( index, true );
            applyExpandState_sub(expandedItems, 
                                 treeView,
                                 model,
                                 model->index( 0, 0, index ) );
        }
    }
}

Then use like:

void myclass::applyExpandState() 
{
    m_treeView->setUpdatesEnabled(false);

    applyExpandState_sub( m_expandedItems,
                          m_treeView,
                          m_model,
                          m_model->index( 0, 0, QModelIndex() ) );

    m_treeView->setUpdatesEnabled(true);
}

I am using the Qt::UserRole here because multiple items in my model can have the same display name which would mess up the expand state restoration, so the UserRole provides a unique identifier for each item to avoid that problem.

iforce2d
  • 8,194
  • 3
  • 29
  • 40
7

These two function by using a loop should do that for you:

QModelIndexList QAbstractItemModel::persistentIndexList () const
bool isExpanded ( const QModelIndex & index ) const
S. Razi
  • 378
  • 2
  • 8
6

Here is a general approach that should work with any QTreeView based widget, that uses some sort of ID system to identify elements (I am assuming the ID is an int, which is stored inside the Qt::UserRole):

void MyWidget::saveExpandedState()
{
    for(int row = 0; row < tree_view_->model()->rowCount(); ++row)
        saveExpandedOnLevel(tree_view_->model()->index(row,0));
}

void Widget::restoreExpandedState()
{
    tree_view_->setUpdatesEnabled(false);

    for(int row = 0; row < tree_view_->model()->rowCount(); ++row)
        restoreExpandedOnLevel(tree_view_->model()->index(row,0));

    tree_view_->setUpdatesEnabled(true);
}

void MyWidget::saveExpandedOnLevel(const QModelIndex& index)
{
    if(tree_view_->isExpanded(index)) {
        if(index.isValid())
            expanded_ids_.insert(index.data(Qt::UserRole).toInt());
        for(int row = 0; row < tree_view_->model()->rowCount(index); ++row)
            saveExpandedOnLevel(index.child(row,0));
    }
}

void MyWidget::restoreExpandedOnLevel(const QModelIndex& index)
{
    if(expanded_ids_.contains(index.data(Qt::UserRole).toInt())) {
        tree_view_->setExpanded(index, true);
        for(int row = 0; row < tree_view_->model()->rowCount(index); ++row)
            restoreExpandedOnLevel(index.child(row,0));
    }
}

Instead of MyWidget::saveExpandedState() and MyWidget::saveExpandedState() one could also directly call MyWidget::saveExpandedOnLevel(tree_view_->rootIndex()) and MyWidget::restoreExpandedOnLevel(tree_view_->rootIndex()). I only used the above implementation because the for loop will be called anyway and MyWidget::saveExpandedState() and MyWidget::saveExpandedState() looked cleaner with my SIGNAL and SLOT design.

Basti Vagabond
  • 1,458
  • 1
  • 18
  • 26
  • I found this very useful! Note to anyone wanting to save and restore more than once, the type containing the ids should be reset every time `saveExpandedState()` is called. Also if you are storing the models that you've created, you can store `QVariant(QModelIndexes)` in Qt::UserRole as your ids. – mrg95 Aug 08 '16 at 09:43
  • Also note that this function will NOT store expanded indexes if a parent index is not also expanded. This may be an issue depending on the use case. – mrg95 Aug 09 '16 at 21:08
2

I have reworked iforce2d's solution into this:

 void ApplyExpandState(QStringList & nodes,
                       QTreeView * view,
                       QAbstractItemModel * model,
                       const QModelIndex startIndex,
                       QString path)
{
    path+=QString::number(startIndex.row()) + QString::number(startIndex.column());
    for(int i(0); i < model->rowCount(startIndex); ++i)
    {
        QModelIndex nextIndex = model->index(i, 0, startIndex);
        QString nextPath = path + QString::number(nextIndex.row()) + QString::number(nextIndex.column());
        if(!nodes.contains(nextPath))
            continue;
        ApplyExpandState(nodes, view, model, model->index(i, 0, startIndex), path);
    }
    if(nodes.contains(path))
        view->setExpanded( startIndex.sibling(startIndex.row(), 0), true );
}

void StoreExpandState(QStringList & nodes,
                      QTreeView * view,
                      QAbstractItemModel * model,
                      const QModelIndex startIndex,
                      QString path)
{
    path+=QString::number(startIndex.row()) + QString::number(startIndex.column());
    for(int i(0); i < model->rowCount(startIndex); ++i)
    {
        if(!view->isExpanded(model->index(i, 0, startIndex)))
            continue;
        StoreExpandState(nodes, view, model, model->index(i, 0, startIndex), path);
    }

    if(view->isExpanded(startIndex))
        nodes << path;
}

This way there is no need to match data. Obviously - for this approach to work, tree needs to stay relatively unchanged. If you somehow change the order of tree items - it will expand wrong nodes.

Zeks
  • 2,265
  • 20
  • 32
  • This code is incorrect for item with row=12, col=3 and item with row=1 and col=23 and analogue combination (we already get "123"). Need generete chunk of path with formatting symbols, for example, "[12,3]". – Xintrea Jan 13 '20 at 12:25
1

Here is a version which doesn't rely on nodes having a unique Qt::UserRole or Qt::DisplayRole - it just serialises the entire QModelIndex

header:

#pragma once
#include <QTreeView>

class TreeView : public QTreeView
{
    Q_OBJECT
public:
    using QTreeView::QTreeView;

    QStringList saveExpandedState(const QModelIndexList&) const;
    void        restoreExpandedState(const QStringList&);
};

source:

#include "tree_view.h"
#include <QAbstractItemModel>

namespace
{
    std::string toString(const QModelIndex& index)
    {
        std::string parent = index.parent().isValid() ? toString(index.parent()) : "X";

        char buf[512];
        sprintf(buf, "%d:%d[%s]", index.row(), index.column(), parent.c_str());
        return buf;
    }

    QModelIndex fromString(const std::string& string, QAbstractItemModel& model)
    {
        int row, column;
        char parent_str[512];
        sscanf(string.c_str(), "%d:%d[%s]", &row, &column, parent_str);

        QModelIndex parent = *parent_str == 'X' ? QModelIndex() : fromString(parent_str, model);

        return model.index(row, column, parent);
    }
}

QStringList TreeView::saveExpandedState(const QModelIndexList& indices) const
{
    QStringList list;
    for (const QModelIndex& index : indices)
    {
        if (isExpanded(index))
        {
            list << QString::fromStdString(toString(index));
        }
    }
    return list;
}

void TreeView::restoreExpandedState(const QStringList& list)
{
    setUpdatesEnabled(false);

    for (const QString& string : list)
    {
        QModelIndex index = fromString(string.toStdString(), *model());
        setExpanded(index, true);
    }

    setUpdatesEnabled(true);
};
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • This doesn't work for any model that supports adding or removing items by any means, because QModelIndexes can and will change by the next time the event loop goes around. QPersistentModelIndex will maintain state and can be kept in the view between calls. – Richard1403832 Sep 21 '21 at 15:15
0

For a QFileSystemModel, you can't use persistentIndexList().

Here is my work around. It works pretty well, even if I do say so myself. I haven't tested to see what happens if you have a slow loading filesystem, or if you remove the file or path.

// scrolling code connection in constructor
model = new QFileSystemModel();

QObject::connect(ui->treeView, &QTreeView::expanded, [=](const QModelIndex &index)
{
    ui->treeView->scrollTo(index, QAbstractItemView::PositionAtTop);//PositionAtCenter);
});

// save state, probably in your closeEvent()
QSettings s;
s.setValue("header_state",ui->treeView->header()->saveState());
s.setValue("header_geometry",ui->treeView->header()->saveGeometry());

if(ui->treeView->currentIndex().isValid())
{
    QFileInfo info = model->fileInfo(ui->treeView->currentIndex());
    QString filename = info.absoluteFilePath();
    s.setValue("last_directory",filename);
}

// restore state, probably in your showEvent()
QSettings s;
ui->treeView->header()->restoreState(s.value("header_state").toByteArray());
ui->treeView->header()->restoreGeometry(s.value("header_geometry").toByteArray());
QTimer::singleShot(1000, [=]() {
    QSettings s;
    QString filename = s.value("last_directory").toString();
    QModelIndex index = model->index(filename);
    if(index.isValid())
    {
        ui->treeView->expand(index);
        ui->treeView->setCurrentIndex(index);

        ui->treeView->scrollTo(index, QAbstractItemView::PositionAtCenter);
        qDebug() << "Expanded" << filename;
    }
    else
        qDebug() << "Invalid index" << filename;
} );

Hope that helps someone.

phyatt
  • 18,472
  • 5
  • 61
  • 80
0

My approach was to save the list of expanded items (as pointers) and when restoring, set the items in this list as expanded.

In order to use the code below, you may need to replace TreeItem* to a constant pointer to your object (that doesn't change after a refresh).

*.h:

protected slots:
    void restoreTreeViewState();
    void saveTreeViewState();
protected:
    QList<TargetObject*> expandedTreeViewItems;

*.cpp:

connect(view->model(), SIGNAL(modelAboutToBeReset()), this, SLOT(saveTreeViewState()));
connect(view->model(), SIGNAL(modelReset()), this, SLOT(restoreTreeViewState()));


...


void iterateTreeView(const QModelIndex & index, const QAbstractItemModel * model,
             const std::function<void(const QModelIndex&, int)> & fun,
             int depth=0)
{
    if (index.isValid())
        fun(index, depth);
    if (!model->hasChildren(index) || (index.flags() & Qt::ItemNeverHasChildren)) return;
    auto rows = model->rowCount(index);
    auto cols = model->columnCount(index);
    for (int i = 0; i < rows; ++i)
        for (int j = 0; j < cols; ++j)
            iterateTreeView(model->index(i, j, index), model, fun, depth+1);
}

void MainWindow::saveTreeViewState()
{
    expandedTreeViewItems.clear();

    iterateTreeView(view->rootIndex(), view->model(), [&](const QModelIndex& index, int depth){
        if (!view->isExpanded(index))
        {
            TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
            if(item && item->getTarget())
                expandedTreeViewItems.append(item->getTarget());
        }
    });
}

void MainWindow::restoreTreeViewState()
{
    iterateTreeView(view->rootIndex(), view->model(), [&](const QModelIndex& index, int depth){
        TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
        if(item && item->getTarget())
            view->setExpanded(index, expandedTreeViewItems.contains(item->getTarget()));
    });
}

I think this implementation gives extra flexibility compared to some of the others here. At least, I could not make it work with my custom model.

If you want to keep new items expanded, change the code to save the collapsed items instead.

Joel Bodenmann
  • 2,152
  • 2
  • 17
  • 44
Adriel Jr
  • 2,451
  • 19
  • 25