Все доброго времени суток! В этой статье хочу рассказать про трудности, с которыми столкнулся при отображении и обновлении древовидной структуры с помощью 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<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, static_cast<int>(srcStart), static_cast<int>(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, static_cast<int>(srcCur), static_cast<int>(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 += static_cast<int>(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 {
VirtualPartAdapter(Part &root);
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.
Замечания и критика категорически приветствуется.
Автор: x512