Оновлення деревоподібної моделі в 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<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.

Зауваження і критика категорично вітається.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.