这是 QML 中的最小可行 TreeView 模型吗?

Is this the Minimum Viable TreeView Model in QML?

我正在制作一个包含三个项目的折叠清单:"Hey"、"Whats" 和 "Up?"。我想把它放到树视图中。我知道这个列表 只会包含这三个项目。 因此,我想知道如何 "nest" 这些项目在一起。

我知道有敏捷系统的实现支持添加和删除 parent/child objects、查找索引...强大的模型。但是,我实际上只需要在 expandable/collapsable 视图中显示这些项目。以下是我阅读的与 C++ 和 QAbstractItemModels 相关的内容:

这是使用模型实现树视图的最简单可行代码:

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 1.4


Window {
    id: mywindow
    visible: true
    width: 640
    height: 480

    TreeView {
        id: treeview
        anchors.fill: parent

        TableViewColumn {
            title: "Phrase"
            role: "phrase"
        }
        model: phraseModel
    }

    ListModel {
        id: phraseModel
        ListElement { phrase: "Hey"; }
        ListElement { phrase: "What's"; }
        ListElement { phrase: "Up?"; }
    }
}

我希望输出结果是这样的嵌套堆栈:

Hey
    What's
        Up?

但我将所有内容都放在一个列中,所有内容都相互对齐:

Hey
What's
Up?

我知道我还没有分配 parents,而且我不完全确定该怎么做 - 但我什至不确定这是否是 需要的 完成此代码。所以我的问题是:将这三个元素堆叠到 expandable/collapsible 视图中缺少的最后一步是什么?

没有可以使用 TreeView 的原生 QML 模型,所以我实现了一个试图通用的模型:

树元素

// treeelement.h
#ifndef TreeElement_H
#define TreeElement_H

#include <QObject>
#include <QQmlListProperty>

class TreeElement : public QObject
{
    Q_OBJECT
public:
    Q_PROPERTY(QQmlListProperty<TreeElement> items READ items)
    Q_CLASSINFO("DefaultProperty", "items")
    TreeElement(QObject *parent = Q_NULLPTR);

    Q_INVOKABLE TreeElement *parentItem() const;
    bool insertItem(TreeElement *item, int pos = -1);
    QQmlListProperty<TreeElement> items();

    TreeElement *child(int index) const;
    void clear();

    Q_INVOKABLE int pos() const;
    Q_INVOKABLE int count() const;

private:
    static void appendElement(QQmlListProperty<TreeElement> *property, TreeElement *value);
    static int countElement(QQmlListProperty<TreeElement> *property);
    static void clearElement(QQmlListProperty<TreeElement> *property);
    static TreeElement *atElement(QQmlListProperty<TreeElement> *property, int index);

    QList<TreeElement *> m_childs;
    TreeElement *m_parent;
};
#endif // TreeElement_H

// treeelement.cpp
#include "treeelement.h"

TreeElement::TreeElement(QObject *parent) :
    QObject(parent),
    m_parent(nullptr) {}

TreeElement *TreeElement::parentItem() const{
    return m_parent;
}


QQmlListProperty<TreeElement> TreeElement::items(){
    return  QQmlListProperty<TreeElement> (this,
                                           this,
                                           &TreeElement::appendElement,
                                           &TreeElement::countElement,
                                           &TreeElement::atElement,
                                           &TreeElement::clearElement);
}

TreeElement *TreeElement::child(int index) const{
    if(index < 0 || index >= m_childs.length())
        return nullptr;
    return m_childs.at(index);
}

void TreeElement::clear(){
    qDeleteAll(m_childs);
    m_childs.clear();
}

bool TreeElement::insertItem(TreeElement *item, int pos){
    if(pos > m_childs.count())
        return false;
    if(pos < 0)
        pos = m_childs.count();
    item->m_parent = this;
    item->setParent(this);
    m_childs.insert(pos, item);
    return true;
}

int TreeElement::pos() const{
    TreeElement *parent = parentItem();
    if(parent)
        return parent->m_childs.indexOf(const_cast<TreeElement *>(this));
    return 0;
}

int TreeElement::count() const{
    return m_childs.size();
}

void TreeElement::appendElement(QQmlListProperty<TreeElement> *property, TreeElement *value){
    TreeElement *parent = qobject_cast<TreeElement *>(property->object);
    parent->insertItem(value);
}

int TreeElement::countElement(QQmlListProperty<TreeElement> *property){
    TreeElement *parent = qobject_cast<TreeElement *>(property->object);
    return parent->count();
}

void TreeElement::clearElement(QQmlListProperty<TreeElement> *property){
    TreeElement *parent = qobject_cast<TreeElement *>(property->object);
    parent->clear();
}

TreeElement *TreeElement::atElement(QQmlListProperty<TreeElement> *property, int index){
    TreeElement *parent = qobject_cast<TreeElement *>(property->object);
    if(index < 0 || index >= parent->count())
        return nullptr;
    return parent->child(index);
}

树模型

// treemodel.h
#ifndef TreeModel_H
#define TreeModel_H

#include <QAbstractItemModel>
#include <QQmlListProperty>

class TreeElement;

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT
public:
    Q_PROPERTY(QQmlListProperty<TreeElement> items READ items)
    Q_PROPERTY(QVariantList roles READ roles WRITE setRoles NOTIFY rolesChanged)
    Q_CLASSINFO("DefaultProperty", "items")

    TreeModel(QObject *parent = Q_NULLPTR);
    ~TreeModel() override;

    QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
    QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
    Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE;
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
    QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
    int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
    int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
    QQmlListProperty<TreeElement> items();

    QVariantList roles() const;
    void setRoles(const QVariantList &roles);

    Q_INVOKABLE QModelIndex indexFromElement(TreeElement *item);
    Q_INVOKABLE bool insertElement(TreeElement *item, const QModelIndex &parent = QModelIndex(), int pos = -1);

    TreeElement *elementFromIndex(const QModelIndex &index) const;

private:
    TreeElement *m_root;
    QHash<int, QByteArray> m_roles;

signals:
    void rolesChanged();
};

#endif // TreeModel_H

// treemodel.cpp
#include "treemodel.h"
#include "treeelement.h"

TreeModel::TreeModel(QObject *parent) :
    QAbstractItemModel(parent){
    m_root = new TreeElement;
}
TreeModel::~TreeModel(){
    delete m_root;
}

QHash<int, QByteArray> TreeModel::roleNames() const{
    return m_roles;
}

QVariant TreeModel::data(const QModelIndex &index, int role) const{
    if (!index.isValid())
        return QVariant();
    TreeElement *item = static_cast<TreeElement*>(index.internalPointer());
    QByteArray roleName = m_roles[role];
    QVariant name = item->property(roleName.data());
    return name;
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const{
    if (!index.isValid())
        return nullptr;
    return QAbstractItemModel::flags(index);
}

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const{
    if (!hasIndex(row, column, parent))
        return QModelIndex();
    TreeElement *parentItem = elementFromIndex(parent);
    TreeElement *childItem = parentItem->child(row);
    if (childItem)
        return createIndex(row, column, childItem);
    else
        return QModelIndex();
}

QModelIndex TreeModel::parent(const QModelIndex &index) const{
    if (!index.isValid())
        return QModelIndex();
    TreeElement *childItem = static_cast<TreeElement*>(index.internalPointer());
    TreeElement *parentItem = static_cast<TreeElement *>(childItem->parentItem());
    if (parentItem == m_root)
        return QModelIndex();
    return createIndex(parentItem->pos(), 0, parentItem);
}

int TreeModel::rowCount(const QModelIndex &parent) const{
    if (parent.column() > 0)
        return 0;
    TreeElement *parentItem = elementFromIndex(parent);
    return parentItem->count();
}

int TreeModel::columnCount(const QModelIndex &parent) const{
    Q_UNUSED(parent)
    return 1;
}

QQmlListProperty<TreeElement> TreeModel::items(){
    return m_root->items();
}

QVariantList TreeModel::roles() const{
    QVariantList list;
    QHashIterator<int, QByteArray> i(m_roles);
    while (i.hasNext()) {
        i.next();
        list.append(i.value());
    }

    return list;
}

void TreeModel::setRoles(const QVariantList &roles){
    static int nextRole = Qt::UserRole + 1;
    for(QVariant role : roles) {
        m_roles.insert(nextRole, role.toByteArray());
        nextRole ++;
    }
    emit rolesChanged();
}

QModelIndex TreeModel::indexFromElement(TreeElement *item){
    QVector<int> positions;
    QModelIndex result;
    if(item) {
        do{
            int pos = item->pos();
            positions.append(pos);
            item = item->parentItem();
        } while(item != nullptr);

        for (int i = positions.size() - 2; i >= 0 ; i--)
            result = index(positions[i], 0, result);
    }
    return result;
}


bool TreeModel::insertElement(TreeElement *item, const QModelIndex &parent, int pos){
    TreeElement *parentElement = elementFromIndex(parent);
    if(pos >= parentElement->count())
        return false;
    if(pos < 0)
        pos = parentElement->count();
    beginInsertRows(parent, pos, pos);
    bool retValue = parentElement->insertItem(item, pos);
    endInsertRows();
    return retValue;
}

TreeElement *TreeModel::elementFromIndex(const QModelIndex &index) const{
    if(index.isValid())
        return static_cast<TreeElement *>(index.internalPointer());
    return m_root;
}

main.cpp

#include "treemodel.h"
#include "treeelement.h"

#include <QGuiApplication>
#include <QQmlApplicationEngine>

static void registertypes(){
    qmlRegisterType<TreeElement>("foo", 1, 0, "TreeElement");
    qmlRegisterType<TreeModel>("foo", 1, 0, "TreeModel");
}

Q_COREAPP_STARTUP_FUNCTION(registertypes)

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);    
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.4

import foo 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TreeModel {
        id: treemodel
        roles: ["phrase"]
        TreeElement{
            property string phrase: "Hey"
            TreeElement{
                property string phrase: "What's"
                TreeElement{
                    property string phrase: "Up?"
                }
            }
        }
    }
    TreeView {
        anchors.fill: parent
        model: treemodel
        TableViewColumn {
            title: "Name"
            role: "phrase"
            width: 200
        }
    }
}

输出:

您找到的完整示例 here

我在 QML 中创建了一个可折叠框架(一个带有标题和内容的组合框)。如果你确定你永远不会改变结构,你可以为你的目的使用:

我通过删除无用的部分(动画、装饰等)简化了代码。所以下面的代码可以改进。但是,我保留了这个想法:

// CollapsibleGroupBox.qml
Item {
    property alias contentItem: content.contentItem;
    property string title: ""
    Item {
        id: titleBar
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.right: parent.right
        height: 30
        Row {
            anchors.fill: parent
            CheckBox {
                Layout.alignment: Qt.AlignLeft
                id: expand
                checked: true;
            }
            Text {
                Layout.alignment: Qt.AlignLeft
                text: title
            }
        }
    }
    Pane {
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: titleBar.bottom
        anchors.bottom: parent.bottom
        topPadding: 0
        visible: expand.checked
        id: content
    }
}
// Main.qml
Item {
    height: 500
    width: 500
    CollapsibleGroupBox {
        anchors.fill: parent
        title: "Hey!"
        contentItem: CollapsibleGroupBox {
            title: "What's"
            contentItem: CollapsibleGroupBox {
                title: "up?"
            }
        }
    }
}

您将获得:

您也可以用 MouseArea 替换复选框。

我还创建了一个仅使用 QML 组件的模型:

import QtQuick 2.9
import QtQuick.Window 2.2
import UISettings 1.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls 1.4 as SV



Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Flickable {
        id: flick
        anchors.fill: parent
        clip: true
        contentHeight: col.implicitHeight
        property var mymodel: {
            "animals": {
                "big": {
                    "land": "elephants",
                    "water": "whales"
                },
                "small": {
                    "land": "mice",
                    "water": "fish"
                }
            },
            "plants": {
                "trees": "evergreens"
            }
        }

        Column {
            id: col
            Component.onCompleted: componentListView.createObject(this, {"objmodel":flick.mymodel});
        }

        Component {
            id: componentListView
            Repeater {
                id: repeater
                property var objmodel: ({})
                model: Object.keys(objmodel)

                ColumnLayout {
                    Layout.leftMargin: 50
                    Button {
                        property var sprite: null
                        text: modelData
                        onClicked: {
                            if(sprite === null) {
                                if(typeof objmodel[modelData] === 'object')
                                sprite = componentListView.createObject(parent, {"objmodel":objmodel[modelData]});
                            }
                            else
                                sprite.destroy()

                        }
                    }
                }
            }
        }
    }
}