Обновление древовидной модели в Qt

Всем доброго времени суток! В этой статье я хочу рассказать про трудности, с которыми столкнулся при отображении и обновлении древовидной структуры с помощью QTreeView и QAbstractItemModel. Так же предложу велосипед, который я создал, чтобы обойти эти трудности.

Для отображения данных Qt использует парадигму ModelView, в которой модель должна реализовываться наследниками QAbstractItemModel. Данный класс сделан удобно, однако поддержка иерархии, как мне показалось, пришита где-то сбоку и не очень удобно. Построить правильную древовидную модель, как разработчики признаются в документации, дело непростое и даже ModelTest, призванный помочь в его отладке, не всегда помогает выявить ошибки в модели.

В моем проекте я столкнулся с еще одной сложностью – с обновлением извне. Дело в том, что QAbstractItemModel требует, что перед любыми действиями с элементами требуется явно указать какие элементы конкретно удаляются, добавляются, перемещаются. Как я пониманию, предполагается, что модель будет редактироваться только посредством View-ов или через методы QAbstractItemModel. Однако, если я работаю с чужой моделью из библиотеки, которая не умеет «правильно» оповещать о своих изменениях, или модель интенсивно редактируется так, что отправлять сообщения об её изменениях становиться накладно, то обновление структуры модели усложняется.

Для решения проблемы такого обновления и упрощения создания реализации QAbstractItemModel. Я решил использовать следующий подход: сделать простой интерфейс для запроса структуры дерева:

class VirtualModelAdapter {
public:
  // запрос структуры
  virtual int getItemsCount(void *parent) = 0;
  virtual void * getItem(void *parent, int index) = 0;
  virtual QVariant data(void *item, int role) = 0;
  // процедуры обновления
  void beginUpdate();
  void endUpdate();
}

и реализовать свою QAbstractItemModel, в которой структура будет кэшироваться и лениво подгружаться по мере необходимости. А обновление модели сделать простой сихранизацией кэшированной структуры с VirtualModelAdapter.

Таким образом, вместо кучи вызовов beginInsertRows/endInsertRows и beginRemoveRows/endRemoveRows можно заключить обновление модели в скобки beginUpdate() endUpdate() и по окончанию обновления выполнять синхронизацию. При этом заметьте – кэшируется только струтура (не данные) и только та её часть, что раскрывается пользователем. Сказано – сделано. Для кэширования дерева я использовал следующую структуру:

class InternalNode {
  InternalNode *parent;
  void *item;
  size_t parentIndex;  
  std::vector<std::unique_ptr<InternalNode>> children;  
}

А для обновления структуры модели я использую функцию, которая сравнивает список элементов и при несовпадении вставляет новые и удаляет ненужные элементы:

void VirtualTreeModel::syncNodeList(InternalNode &node, void *parent)
{
  InternalChildren &nodes = node.children;
  int srcStart = 0;
  int srcCur = srcStart;
  int destStart = 0;

  auto index = getIndex(node);
  while (srcCur <= static_cast<int>(nodes.size()))
  {
    bool finishing = srcCur >= static_cast<int>(nodes.size());
    int destCur = 0;
    InternalNode *curNode = nullptr;
    if (!finishing) {
      curNode = nodes[srcCur].get();
      destCur = m_adapter->indexOf(parent, curNode->item, destStart);
    }
    if (destCur >= 0)
    {
      // remove skipped source nodes
      if (srcCur > srcStart)
      {
        beginRemoveRows(index, srcStart, srcCur-1);
        node.eraseChildren(nodes.begin() + srcStart, nodes.begin() + srcCur);
        if (!finishing)
          srcCur = srcStart;
        endRemoveRows();
      }
      srcStart = srcCur + 1;

      if (finishing)
        destCur = m_adapter->getItemsCount(parent);
      // insert skipped new nodes
      if (destCur > destStart)
      {
        int insertCount = destCur - destStart;
        beginInsertRows(index, srcCur, srcCur + insertCount - 1);
        for (int i = 0, cur = srcCur; i < insertCount; i++, cur++)
        {
          void *obj = m_adapter->getItem(parent, destStart + i);
          auto newNode = new InternalNode(&node, obj, cur);
          nodes.emplace(nodes.begin() + cur, newNode);
        }
        node.insertedChildren(srcCur + insertCount);
        endInsertRows();

        srcCur += insertCount;
        destStart += insertCount;
      }
      destStart = destCur + 1;

      if (curNode && curNode->isInitialized(m_adapter))
      {
        syncNodeList(*curNode, curNode->item);
        srcStart = srcCur + 1;
      }
    }
    srcCur++;
  }
  node.childInitialized = true;
}

По сути получается следующая система: когда структура данных начинает меняться после вызова BeginUpdate(), все обращения View к index(), parent() и т.п. транслируются к кэшу, а data() возвращает пустой QVariant(). По завершению обновления структуры вы вызываете endUpdate() и происходит синхронизация со всеми вставками и удалениями и View перерисовывается.

В качестве примера я сделал следующую структуру разделов:

class Part {
  Part *parent;
  QString name;
  std::vector<std::unique_ptr<Part>> subParts;
}

Теперь для её отображения мне достаточно реализовать следующий класс:

сlass VirtualPartAdapter : public VirtualModelAdapter {
  int getItemsCount(void *parent) override;
  void * getItem(void *parent, int index) override;
  QVariant data(void *item, int role) override;
  void * getItemParent(void *item) override;
  Part *getValue(void * data);
};


А для любых изменений извне используем следующий подход:

  m_adapter->beginUpdate();
  Part* cur = currentPart();
  auto g1 = cur->add("NewType");
  g1->add("my class");
  g1->add("my struct");
  m_adapter->endUpdate();

В качестве еще более простой альтернативы можно вызвать QueuedUpdate() перед изменением данных и тогда обновление структуры произойдет автоматически после обработки сигнала, посланного через Qt::QueuedConnection:

  m_adapter-> QueuedUpdate();
  Part* cur = currentPart();
  auto g1 = cur->add("NewType");
  g1->add("my class");
  g1->add("my struct");


Заключение


Мой опыт работы с C++ и Qt не велик и меня не покидает ощущение, что проблему можно решить проще. В любом случае, надеюсь, этот способ будет кому-нибудь полезен. С полным текстом и примером можно ознакомиться на github.

Замечания и критика категорически приветствуются.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    0
    У вас были какие-то проблемы с производительностью, что понадобилось кэширование?
    Если меняется так много данных, может стоит использовать beginResetModel()?
      0
      Такая схема мне понадобилась потому что мои данные часто обновляются извне (не через View) и моя структура не может отправлять данные об изменении (она не знает об QAbstractItemModel и индексах). А beginResetModel() не может быть мне другом — мне нужно, чтобы все выделенные и раскрытые узлы модели вместе с полосами прокрутки остались на месте. Данные у меня меняются зачастую немного, но иногда очень значительно и хочется, чтобы для небольших изменений программа была дружелюбна к пользователю.
        0
        Помимо этого удобство этого адаптера заключается в том, что при его использовании не нужно тянуть никакую логику по обновлению древовидной модели через beginInsertRows/endInsertRows, beginRemoveRows/endRemoveRows рискуя потратить кучу времени на отладку — теперь вместо этого можно сделать beginUpdate() / endUpdate() и всё это сделается автоматом без лишней головной боли и с минимальным оверхедом
          0
          На самом деле обработка сигналов roswInserted/RowsDeleted не то чтобы дорогая во view, потому что view не начинает все перерисовывать, а откладывает обновление на некоторый промежуток времени, так что если идет большой поток изменений, то перерисовано окно будет всего один раз. Так что я не уверен, что такой подход даст большой выигрыш в производительности, если вообще даст.

          Но вообще подход имеет место, в случае если таким образом удобно делать мост с «настоящей» моделью, хотя предпочтительнее все же заставить вашу структуру смочь отправлять данные об изменении, пусть она и не знает при этом ничего об индексах.
            0
            Согласен, но иногда эти изменения могут иметь значения, если обновлений достаточно много, т.к. rowsInserted/RowsDeleted каждый раз обновляют весь список QPersistenIndex-ов на модель. И если у меня будет десяток тысяч обновлений внутри скрытого узла, то это добавит лишнюю, хоть и незначительную задержку (у меня так происходит во время работы пользовательских скриптов). В моем же случае если все эти изменения происходят внутри скрытого узла — то модель не получит ни единого обновления.
              0
              зато если пользователь предварительно хотя бы раз развернет все узлы, то ваш подход, как я понял, будет серьезно тормозить. А количество QPersistenIndex-ов не велико обычно — только выделение же. Обычно это 1 индекс, если можно выделять несколько — то с десяток. Редко больше.
                0
                если в дереве мало изменений, то синхронизация отработает быстро даже для сравнительно большого дерева, ну а если изменений много, то тут уж ничего не поделаешь
                  0
                  разве вам не нужно пройтись по всему загруженному дереву, чтобы проверить, что изменилось? И еще я не понял, как dataСhanged обрабатывается
                    0
                    нужно, но сколько бы ни было изменений сравнение пройдет лишь раз по дереву. В моем случае, при количестве узлов в пределах 20-30 тысяч это происходит очень быстро. А изменение данных происходит через emit dataChanged(QModelIndex(), QModelIndex()); по окончанию синхронизации
                  0
                  количество выделенных элементов (а следовательно QPersistentIndex-ов) у меня может быть очень значительно — сотня-другая элементов запросто, а то и под тысячу. Я нас конструкторская программа и пользователь может на 3d модели выделять для редактирования значительное количество элементов и выделенные элементы должны быть выделенными и в дереве
            0
            не туда
              0
              Когда переключаешься на Qt с, например .NET и WPF, то модели вызывают, наверное, больше всего вопросов. Вот есть у нас объекты с данными и есть контролы которые их отображают. Необходимость каждый раз создавать еще и специального посредника между ними — раздражает. Подход когда данные реализуют интерфейсы для уведомления о своем изменении (типа INotifyPropertyChanged и INotifyCollectionChanged), а контрол посредством биндингов привязывается напрямую к свойствам объектов данных кажется более удачным.
                0
                В QtQuick простые модели можно описывать просто как QQmlPropertyList и кидать сигналы, что список изменился, объекты же кидают сигналы об изменении своих свойств. Но в случае сложных древовидных структур это уже работает не так оптимально и желательно делать наследника QAbstractItemModel.

              Only users with full accounts can post comments. Log in, please.