Но иногда не нужно строить большие деревья, где оправдано изменение rootIndex модели, а нужно построить дерево с небольшим ветвлением и с возможностью отображения его целиком.
Ниже описан способ как это сделать.
Ограничения
- Данный способ будет работать медленно на моделях с большим ветвлением и/или большим количеством вложенных элементов.
- Данный способ использует C++-модели, что ограничивает возможности его применения.
Описание метода
Мы не будем писать полноценное tree-представление для QML, а ограничимся его имитацией. Имитировать будем посредством вставки/удаления элементов-потомков в обычную линейную модель. То есть, у нас есть линейная модель:
- Element 1
- Element 2
- Element 3
- Element 1
- Element 2
- Element 2.1
- Element 2.2
- Element 3
Что в итоге получится
Сразу приведу скриншоты результата, чтобы знать, что мы хотим получить.
Все элементы свернуты | Развернут один элемент | Все элементы развернуты |
Реализация части на QML
Начнем разработку с реализации декларативной части.
import Qt 4.7 ListView { id: treeView width: 500 height: 500 //Задаем делегата delegate: treeDelegate //Задаем модель, этот объект позже придет из C++ model: treeModel
//Компонент делегата Component { id: treeDelegate Item { id: wrapper height: 50 width: treeView.width //Полоска для отделения элементов друг от друга Rectangle { height: 1 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right color: "#d0d0d0" }
//"Отбой" слева элементов-потомков Item { id: levelMarginElement //Начиная с 6 уровня вложенности не сдвигаем потомков, //так как иначе можно получить очень широкое окно width: (level>5?6:level)*32 + 5 anchors.left: parent.left }
//Область для открытия/закрытия потомков. //На листьях не виден Item { id: nodeOpenElement anchors.left: levelMarginElement.right anchors.verticalCenter: wrapper.verticalCenter height: 24 state: "leafNode" Image { id: triangleOpenImage
//Отлавливаем нажатие мышкой и открываем/закрываем элемент MouseArea { anchors.fill: parent onClicked: { (isOpened) ? treeModel.closeItem(index) : treeModel.openItem(index) } }
}
states: [ //Лист //Область не видна State { name: "leafNode" when: !hasChildren PropertyChanges { target: nodeOpenElement visible: false width: 0 }
},
//Открытый элемент //Область видна и отображена соответствующая иконка State { name: "openedNode" when: (hasChildren)&&(isOpened) PropertyChanges { target: nodeOpenElement visible: true width: 32 }
PropertyChanges { target: triangleOpenImage source: "qrc:/images/tree-node-opened.png" }
},
//Закрытый элемент //Область видна и отображена соответствующая иконка State { name: "closedNode" when: (hasChildren)&&(!isOpened) PropertyChanges { target: nodeOpenElement visible: true width: 32 }
PropertyChanges { target: triangleOpenImage source: "qrc:/images/tree-node-closed.png" }
}
]
}
//Область для отображения данных элемента Text { id: nameTextElement text: name verticalAlignment: "AlignVCenter" anchors.left: nodeOpenElement.right anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right }
}
}
} |
По сути, весь компонент состоит из ListView, в котором реализован делегат.
Делегат состоит из:
- "Отбоя" дочерних элементов, который нужен для сдвига потомков относительно предка вправо. Причем сделано ограничение на 6 уровней сдвига, чтобы не получилось очень широкого окна в случае большой вложенности. Механизм аналогичен механизму комментариев на хабре.
- Области открытия/закрытия элемента. В данной области отображается плюс/минус, если у элемента есть потомки и ничего не отображается (а точнее область просто скрывается), если это лист.
- Области отображения содержимого элемента. В нашем случае просто текстовая строка.
Реализация части на C++
Теперь рассмотрим, что нам надо реализовать на C++. Нам нужна модель (наследник QAbstractListModel) и проброс ее экземпляра в QML-код. Начнем с модели.
Объявление модели (treemodel.h):
#ifndef TREEMODEL_H #define TREEMODEL_H #include < QAbstractListModel> class TreeModelItem; class TreeModel : public QAbstractListModel {
Q_OBJECT public: explicit TreeModel(QObject *parent=0); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const; public slots: void openItem(int numIndex); void closeItem(int numIndex); private: Q_DISABLE_COPY(TreeModel); QList<TreeModelItem *> items; enum ListMenuItemRoles { NameRole = Qt::UserRole+1, LevelRole, IsOpenedRole, HasChildrenRole
}; };
#endif // TREEMODEL_H |
В модель, кроме обязательных к переопределению data() и rowCount(), добавим еще два метода openItem() и closeItem(). Они объявлены слотами, чтобы можно было достучаться до них из QML-кода. Также добавим 4 роли к имеющимся стандартным для получения доступа к параметрам элементов модели.
В файле реализации (treemodel.cpp) опишем еще один класс, который хранит один элемент модели:
class TreeModelItem {
public: TreeModelItem(QString name_ = QString()) : name(name_), level(0), isOpened(false) {} void adjustChildrenLevels() { foreach(TreeModelItem *item, children) { item->level = level+1; item->adjustChildrenLevels(); } } QString name; int level; bool isOpened; QList<TreeModelItem *> children; inline bool hasChildren() {return !children.empty();} };
|
Этот класс кроме параметров, также имеет один метод adjustChildrenLevels(), с помощью которого мы рекурсивно проставляем уровни вложенности (level) у всех потомков элемента. В реальном коде он скорее всего будет удален и расчет уровня будет происходить автоматически при добавлении элемента в дерево, но в примере он уменьшает код и упрощает общую картину.
Конструктор модели:
TreeModel::TreeModel(QObject *parent) : QAbstractListModel(parent) {
TreeModelItem *toAdd; items << new TreeModelItem("First"); toAdd = new TreeModelItem("Child 1.1"); toAdd->children << new TreeModelItem("Child 1.1.1"); items[0]->children << toAdd; items[0]->adjustChildrenLevels(); items << new TreeModelItem("Second"); items << new TreeModelItem("Third"); items << new TreeModelItem("Fourth"); toAdd = new TreeModelItem("Fifth"); toAdd->children << new TreeModelItem("Child 5.1"); toAdd->adjustChildrenLevels(); items << toAdd; QHash<int, QByteArray> roles = roleNames(); roles.insert(NameRole, QByteArray("name")); roles.insert(LevelRole, QByteArray("level")); roles.insert(IsOpenedRole, QByteArray("isOpened")); roles.insert(HasChildrenRole, QByteArray("hasChildren")); setRoleNames(roles); }
|
В конструкторе мы заносим тестовые элементы в модель и регистрируем наши дополнительные роли (под этими именами они будут доступны в QML).
Переопределение методов класса-предка:
QVariant TreeModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) return QVariant(); if (index.row() > (items.size()-1) ) return QVariant(); TreeModelItem *item = items.at(index.row()); switch (role) { case Qt::DisplayRole: case NameRole: return QVariant::fromValue(item->name); case LevelRole: return QVariant::fromValue(item->level); case IsOpenedRole: return QVariant::fromValue(item->isOpened); case HasChildrenRole: return QVariant::fromValue(item->hasChildren()); default: return QVariant(); } }
int TreeModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent) return items.size(); }
|
Метод rowCount() не представляет интереса. Метод data() по пришедшей в параметре роли возвращает нужные данные.
Метод открытия элемента:
void TreeModel::openItem(int numIndex) {
if (numIndex > (items.size()-1)) return; if (items[numIndex]->isOpened) return; QModelIndex modelIndex = index(numIndex); //Выставляем флаг открытого элемента items[numIndex]->isOpened = true; //Оповещаем QML-код об изменении данных emit dataChanged(modelIndex, modelIndex); int i = numIndex+1; //Оповещаем QML-код о том, что будут добавлены строки в модель beginInsertRows(QModelIndex(), i, i+items[numIndex]->children.size()-1); //Добавляем всех потомков элемента в модель после этого элемента foreach(TreeModelItem *item, items[numIndex]->children) items.insert(i++, item); //Оповещаем QML-код о том, что все строки добавлены endInsertRows(); }
|
Метод openItem() выставляет флаг открытого элемента и по очереди добавляет всех потомков текущего элемента в модель после этого элемента.
Метод закрытия элемента:
void TreeModel::closeItem(int numIndex) {
if (numIndex > (items.size()-1)) return; if (!items[numIndex]->isOpened) return; QModelIndex modelIndex = index(numIndex); //Сбрасываем флаг открытого элемента items[numIndex]->isOpened = false; //Оповещаем QML-код об изменении данных emit dataChanged(modelIndex, modelIndex); int i = numIndex+1; //Ищем все элементы после текущего с большим level //Таким образом найдем всех прямых и косвенных потомков for (; i < items.size() && (items[i]->level > items[numIndex]->level); ++i) {} --i; //Оповещаем QML-код о том, что будут удалены строки из модели beginRemoveRows(QModelIndex(), numIndex+1, i); //Удаляем все посчитанные элементы из модели и сбрасываем у них флаг открытия while (i > numIndex) { items[i]->isOpened = false; items.removeAt(i--); } //Оповещаем QML-код о том, что все строки удалены endRemoveRows(); }
|
Метод closeItem() сбрасывает флаг открытого элемента у текущего элемента и у всех его прямых и косвенных потомков (чтобы при повторном открытии не учитывать возможных открытых потомков, а считать что они все закрыты), ищет в модели все элементы с level большим чем у текущего элемента и удаляет их из модели.
Осталось только отобразить QML и передать ему экземпляр нашей модели (main.cpp):
#include < QtGui/QApplication> #include < QtDeclarative> #include "treemodel.h" int main(int argc, char *argv[]) {
QApplication a(argc, argv); QDeclarativeView view; TreeModel *model = new TreeModel; view.rootContext()->setContextProperty("treeModel", model); view.setSource(QUrl("qrc:/Main.qml")); view.show(); return a.exec(); }
|
Заключение
Вышеприведенный код это только пример реализации и содержит множество упрощений, которые вряд ли будут допустимы в реальном применении. Тем не менее, он отображает общую идею способа.
Замечательно.
ОтветитьУдалитьЕсли будет много таких отличных технических статей. Буду постоянным читателем.
Fastman, статьи конечно будут, но не регулярно.
ОтветитьУдалитьЯ как раз мучаюсь построить Трее (используя MVC) из 4 таблиц бд.
ОтветитьУдалитьТаблицы имеют структуру:
1.Категорий товаров (ИД,Наименование ....)
2.Релэшыны (связки между категориями Парент-Чилд)
3.Товары (ИД...)
4.Релэшыны (связки между товарами и категориями)
На форумах нашел тему для меня ... немного переделал запрос (выбираю из 2 таблиц) ... а вот как прикрутить еще 2 никак не пойму ...
Кто может помочи?
victor, ну вопрос не совсем по теме поста, так что советую вам спросить на форуме prog.org.ru или на developer.qt.nokia.com .
ОтветитьУдалитьСпасибо! Как раз срочно понадобился такой компонент.
ОтветитьУдалитьПознавательно.
ОтветитьУдалитьА как лучше реализовать такое дерево для контакт-листа?
типа
group_1
- contact_1_1
- contact_1_2
group_2
- contact_2_1
- contact_2_2
c учётом того, что высота и дизайн итема group и contact разные...
Можно, например, сделать в делегате Loader, который будет грузить разные .qml файлы для разных типов.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьЖалко, что QML умеет только перебирать индексы и не умеет использовать вложенность в AbstractItemModel. То есть для нескольких ListView, идущих друг за другом приходится создавать QList (требуется одним классом выгребать данные всех ListView).
ОтветитьУдалить