Всем доброго времени суток! В этой статье я хочу рассказать про трудности, с которыми столкнулся при отображении и обновлении древовидной структуры с помощью QTreeView и QAbstractItemModel. Так же предложу велосипед, который я создал, чтобы обойти эти трудности.
Для отображения данных Qt использует парадигму ModelView, в которой модель должна реализовываться наследниками QAbstractItemModel. Данный класс сделан удобно, однако поддержка иерархии, как мне показалось, пришита где-то сбоку и не очень удобно. Построить правильную древовидную модель, как разработчики признаются в документации, дело непростое и даже ModelTest, призванный помочь в его отладке, не всегда помогает выявить ошибки в модели.
В моем проекте я столкнулся с еще одной сложностью – с обновлением извне. Дело в том, что QAbstractItemModel требует, что перед любыми действиями с элементами требуется явно указать какие элементы конкретно удаляются, добавляются, перемещаются. Как я пониманию, предполагается, что модель будет редактироваться только посредством View-ов или через методы QAbstractItemModel. Однако, если я работаю с чужой моделью из библиотеки, которая не умеет «правильно» оповещать о своих изменениях, или модель интенсивно редактируется так, что отправлять сообщения об её изменениях становиться накладно, то обновление структуры модели усложняется.
Для решения проблемы такого обновления и упрощения создания реализации QAbstractItemModel. Я решил использовать следующий подход: сделать простой интерфейс для запроса структуры дерева:
и реализовать свою QAbstractItemModel, в которой структура будет кэшироваться и лениво подгружаться по мере необходимости. А обновление модели сделать простой сихранизацией кэшированной структуры с VirtualModelAdapter.
Таким образом, вместо кучи вызовов beginInsertRows/endInsertRows и beginRemoveRows/endRemoveRows можно заключить обновление модели в скобки beginUpdate() endUpdate() и по окончанию обновления выполнять синхронизацию. При этом заметьте – кэшируется только струтура (не данные) и только та её часть, что раскрывается пользователем. Сказано – сделано. Для кэширования дерева я использовал следующую структуру:
А для обновления структуры модели я использую функцию, которая сравнивает список элементов и при несовпадении вставляет новые и удаляет ненужные элементы:
По сути получается следующая система: когда структура данных начинает меняться после вызова BeginUpdate(), все обращения View к index(), parent() и т.п. транслируются к кэшу, а data() возвращает пустой QVariant(). По завершению обновления структуры вы вызываете endUpdate() и происходит синхронизация со всеми вставками и удалениями и View перерисовывается.
В качестве примера я сделал следующую структуру разделов:
Теперь для её отображения мне достаточно реализовать следующий класс:
А для любых изменений извне используем следующий подход:
В качестве еще более простой альтернативы можно вызвать QueuedUpdate() перед изменением данных и тогда обновление структуры произойдет автоматически после обработки сигнала, посланного через Qt::QueuedConnection:
Мой опыт работы с C++ и Qt не велик и меня не покидает ощущение, что проблему можно решить проще. В любом случае, надеюсь, этот способ будет кому-нибудь полезен. С полным текстом и примером можно ознакомиться на github.
Замечания и критика категорически приветствуются.
Для отображения данных 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.
Замечания и критика категорически приветствуются.