Is this the Minimum Viable TreeView Model in QML?

2020-04-19 23:57发布

问题:

I'm making a folding list of three items: "Hey", "Whats", and "Up?". I want to put it into a tree view. I know this list will only ever contain these three items. Therefore, I would like to know how to "nest" these items together.

I know there are implementation for agile systems that support adding and removing parent/child objects, finding indexes... powerful models. However, I literally only need to display these items in an expandable/collapsable view. Here is what I've read through relating to C++ and QAbstractItemModels:

  • QML Treeview
  • QML QAbstractItemModel
  • This Question by My_Cat_Jessica
  • This question by kavaliero which was based on:
  • This 'working' example by Qt themselves (Doesn't actually work for TreeView. Works for QTreeView though!)

Here is the simplest viable code to implement a treeview with model:

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?"; }
    }
}

I would like for the output to result in a nested stack like this:

Hey
    What's
        Up?

But I am getting everything in a single column all aligned with each other:

Hey
What's
Up?

I know I haven't assigned parents, and I'm not entirely sure how to do that - But I'm not even sure if that's what needs done to this code. So my question is this: What's the final step missing to stack these three elements into an expandable/collapsible view?

回答1:

There is no native QML model that can use the TreeView, so I have implemented a model that tries to be generic:

TreeElement

// 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

// 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
        }
    }
}

Output:

The complete example you find here



回答2:

I created a collapsible frame in QML (a group box with a title and a content). If you are sure that you will never change the structure, you can use for your purpose:

I simplified the code by removing the useless parts (animations, decorations, etc.). So the code below could be improved. But, I kept the idea:

// 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?"
            }
        }
    }
}

You will get:

You can replace the checkbox by a MouseArea, also.



回答3:

I've also created a model that only uses QML components:

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()

                        }
                    }
                }
            }
        }
    }
}