[Из песочницы] Обновление древовидной модели в 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
auto index = getIndex (node);
while (srcCur <= static_cast
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
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.Замечания и критика категорически приветствуются.