qml中树形结构控件TreeView的使用,以及其继承QAbstractItemModel的model的使用方式

qml中树形结构控件TreeView的使用,以及其继承QAbstractItemModel的model的使用方式

不想看过程的,直接跳到最后面有源码

先看效果图:
在这里插入图片描述
树形结构图,我经常不是很喜欢用它,因为它需要的model太麻烦了,还得我自己写个类,相比起来我更喜欢ListView,但有时候TreeView也不得不用,毕竟数据结构如此啊。
所以为了下次使用TreeView方便,我记录一下这个model如何使用。

官方文档记载:
在这里插入图片描述
model是一个QAbstractItemModel继承类,所以我们必须要先写一个类,将这个类暴露给qml的TreeView控件。

什么是QAbstractItemModel?

官方:
QAbstractItemModel类定义了项目模型必须使用的标准接口,以便能够与模型/视图体系结构中的其他组件进行互操作。它不应该被直接实例化。相反,您应该子类化它来创建新模型。
QAbstractItemModel类是模型/视图类之一,是Qt模型/视图框架的一部分。它可以用作QML中的项视图元素或Qt Widgets模块中的项视图类的底层数据模型。
在这里插入图片描述
再图片中,row为行,每一行即为一个节点,例如:一本书、一卷、一章;而column为列,每一个节点都有多个数据值,一个数据值占据一列,例如:一章里有章名、状态、时间等。

创建model需要的QAbstractItemModel继承类

创建 TreeViewModel.h 及 TreeViewModel.cpp,继承 QAbstractItemModel 类。

// TreeViewModel.h

#include <QAbstractItemModel>
#include <QObject>

class TreeViewModel : public QAbstractItemModel
{
	Q_OBJECT
public:
    TreeViewModel(const QStringList& headers, QObject *parent = NULL);
    
private:
    QStringList m_headers;
}
// TreeViewModel.cpp
#include "TreeViewModel.h"

TreeViewModel::TreeViewModel(const QStringList& headers, QObject *parent)
{
    this->m_headers = headers;
}

好,这个时候,我们去main.cpp上,去new一个模型,看能用吗。

// main.cpp
#include <QQmlContext>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "TreeViewModel.h"

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    // 把模型和qml对接的c++类暴露给qml
    TreeViewModel *model = new TreeViewModel(QStringList());
    engine.rootContext()->setContextProperty("myModel", model);

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

这个时候你会发现它是报错了的,因为有五个必须要重写的函数没有完成,它们分别是:

public: 
	// 在parent节点下,第row行,第column列位置上创建索引
	Q_INVOKABLE QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
	// 返回给定节点的父节点
	Q_INVOKABLE QModelIndex parent(const QModelIndex &child) const override;
	// 返回给定节点的行数
	Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
	// 返回给定节点的列数
	Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const override;
	// 返回给定节点的指定角色数据
	Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

这些函数是干什么的?TreeView要显示模型数据,它总得知道这个模型里面的数据有多少行多少列吧,知道后才能获取这个节点数据,然后再根据这个节点获取它的子节点有多少行多少列,再去获取每行每列的数据。
因为TreeView是在qml下的,所以为了能够调用cpp里的函数,必须要用修饰符Q_INVOKABLE。

节点数据

刚我们说到要TreeView要获取数据,那么数据要怎么放?从函数名来看,QVariant data(const QModelIndex &index, int role = Qt::DisplayRole)这个函数就可以获取数据了,但是TreeView要怎么知道我哪个数据是Name,哪个数据是State呢?还有,我需要在节点下添加子节点,要怎么做呢?

创建节点数据函数

节点数据函数是专门用来管控节点的数据的,至于哪个是子节点哪个是父节点,这不都是相对的吗。比如说,‘第一卷’相对于 ‘第一章’ 来说它就是父节点,但 ‘第一卷’ 相对于 ‘第一本书’ 来说它就是子节点。
创建 TreeViewItem.h 和 TreeViewItem.cpp。

// TreeViewItem.h

class TreeViewItem
{
public:
    TreeViewItem(const QList<QVariant> &data, TreeViewItem* parent=NULL);
    ~TreeViewItem();
    void appendChild(TreeViewItem *child);
    void deleteAllChild();

    TreeViewItem *child(int row);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    void setParent(TreeViewItem *parent);
    TreeViewItem *parent();
    int row() const;


private:
    // 因为每一个节点都只有一个父节点,但是可能有很多个孩子,例如每一卷都有第一章、第二章、第三章等,那么相对来说,卷就是父节点,章就是子节点
    TreeViewItem *m_parentItem; // 这是本节点的父节点
    QList<TreeViewItem *> m_childItems; // 这是本节点的子节点列表
    QList<QVariant> m_itemData; // 这是本节点的数据区域,一个节点可能会有多个数据,例如书名、修改时间、是否保存等

};

因为是节点控制,所以把节点上增加子节点、删除子节点、获取子节点、子节点数量、数据等都要考虑进来。
而它的数据中, 需要知道该节点的父节点、子节点列表,以及本身节点的数据。

完善QAbstractItemModel继承类

节点数据有了,那之前定义的TreeViewModel怎么完善?
先在TreeViewModel.h的private中增加一个节点数据TreeViewItem *rootItem,这是整个TreeView的头节点, 然后再完成那五个必重写的函数。
再创建一个enum,里面放两个数据 NAME 和 STATE,对应着我ui上面的NAME和STATE。

// TreeViewModel.h
#include <QAbstractItemModel>
#include <QObject>
#include <QDebug>
#include "TreeViewItem.h"

#pragma execution_character_set("utf-8")

class TreeViewModel : public QAbstractItemModel
{
    Q_OBJECT
    enum ItemRoles {
        NAME = Qt::UserRole + 1,
        STATE
    };
public:
    TreeViewModel(const QStringList& headers, QObject *parent = NULL);
    ~TreeViewModel();

    // 必须要重写的五个函数
    Q_INVOKABLE QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE QModelIndex parent(const QModelIndex &child) const override;
    Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;


private:
    TreeViewItem *itemFromIndex(const QModelIndex &index) const;

private:
    TreeViewItem *rootItem;
    QStringList m_headers;
};
// TreeViewModel.cpp
#include "TreeViewModel.h"

// 返回给定父节点和角色的节点的索引
TreeViewModel::TreeViewModel(const QStringList& headers, QObject *parent)
{
    this->m_headers = headers;
    this->rootItem = new TreeViewItem(QVariantList());
}

TreeViewModel::~TreeViewModel()
{
    rootItem->deleteAllChild();
    rootItem = NULL;
}

// 在parent节点下,第row行,第column列位置上创建索引
QModelIndex TreeViewModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
    {
        return QModelIndex();
    }

    TreeViewItem *treeItem = itemFromIndex(parent);
    TreeViewItem *child = treeItem->child(row); // 第几行就是第几个子节点,TreeView一般都是一行一个节点的
    if (child) {
        return createIndex(row, column, child);
    } else {
        return QModelIndex();
    }

}

// 返回给定节点的父节点
QModelIndex TreeViewModel::parent(const QModelIndex &child) const
{
    if (!child.isValid())
    {
        return QModelIndex();
    }

    TreeViewItem *childItem = static_cast<TreeViewItem*>(child.internalPointer());
    TreeViewItem *parentItem = childItem->parent();

    if (parentItem == rootItem)
    {
        return QModelIndex();
    }

    return createIndex(parentItem->row(), 0, parentItem);
}

// 返回给定节点的行数
int TreeViewModel::rowCount(const QModelIndex &parent) const
{
    if (parent.column() > 0)
        return 0;

    TreeViewItem* item = itemFromIndex(parent);
    return item->childCount();
}

// 返回给定节点的列数
int TreeViewModel::columnCount(const QModelIndex &parent) const
{
    return 2;
    // 返回实际的列数 (实际是他返回了0,因为根节点用的是无参的构造),TreeView控件会认为表是空表,不获取数据
    if (parent.isValid())
    {
        return static_cast<TreeViewItem*>(parent.internalPointer())->columnCount();
    } else {
        return rootItem->columnCount();
    }
}

// 返回给定节点的指定角色数据
QVariant TreeViewModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    TreeViewItem *item = itemFromIndex(index);

    switch (role) {
    case NAME:
        return item->data(0);
    case STATE:
        return item->data(1);
    default:
        return QVariant();
    }

}

// 获取父节点的数据指针,如果parent可用就用parent的节点,如果不可用就用总父节点
TreeViewItem *TreeViewModel::itemFromIndex(const QModelIndex &parent) const
{
    if (parent.isValid())
    {
        // internalPointer: 返回一个void *指针,用于模型将索引与内部数据结构关联起来
        TreeViewItem *item = static_cast<TreeViewItem*>(parent.internalPointer());
        return item;
    }
    return rootItem;
}

看我data函数中,NAME的数据为item->data(0),STATE的数据为item->data(1),这也就意味着,我在新建子节点数据时,需要将name的数据放在第一位,state的数据放在第二位, 当然严谨点的话,可以做一个struct数据结构,我这也就是随便一写,就这么个意思而已

那,qml上的TreeView,又怎么知道我定义的NAME和STATE?怎么跟qml上的数据绑定起来?
QAbstractItemModel上还有一个函数叫做roleNames(),跟TreeView上的role属性相对应。

public:
	Q_INVOKABLE QHash<int, QByteArray> roleNames() const override;
QHash<int, QByteArray> TreeViewModel::roleNames() const
{
    QHash<int, QByteArray> names(QAbstractItemModel::roleNames());
    names[NAME] = "name";
    names[STATE] = "state";
    return names;
}

在qml上为:

TreeView {
    anchors.fill: parent
    model: myModel

    TableViewColumn {
        title: "Name"
        role: "name"
        width: 150
    }

    TableViewColumn {
        title: "State"
        role: "state"
        width: 150
    }
}

源码

注意:我在.h文件里的 #pragma execution_character_set(“utf-8”) 主要是为了使中文不乱码,或许每个电脑的编码不一样,代码也不一样哦,或者可以直接使用英文……
源码结构:
在这里插入图片描述
TreeViewItem.h:

// TreeViewItem.h
#ifndef TREEVIEWITEM_H
#define TREEVIEWITEM_H

#include <QVariant>

#pragma execution_character_set("utf-8")

class TreeViewItem
{
public:
    TreeViewItem(const QList<QVariant> &data, TreeViewItem* parent=NULL);
    ~TreeViewItem();
    void appendChild(TreeViewItem *child);
    void deleteAllChild();

    TreeViewItem *child(int row);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    void setParent(TreeViewItem *parent);
    TreeViewItem *parent();
    int row() const;


private:
    // 因为每一个节点都只有一个父节点,但是可能有很多个孩子,例如每一卷都有第一章、第二章、第三章等,那么相对来说,卷就是父节点,章就是子节点
    TreeViewItem *m_parentItem; // 这是本节点的父节点
    QList<TreeViewItem *> m_childItems; // 这是本节点的孩子节点列表
    QList<QVariant> m_itemData; // 这是本节点的数据区域,一个节点可能会有多个数据,例如书名、修改时间、是否保存等

};

#endif // TREEVIEWITEM_H

TreeViewItem.cpp

// TreeViewItem.cpp
#include "TreeViewItem.h"

TreeViewItem::TreeViewItem(const QList<QVariant> &data, TreeViewItem *parent)
{
    this->m_itemData = data;
    this->m_parentItem = parent;
}

TreeViewItem::~TreeViewItem()
{
    this->m_itemData.clear();
    this->m_parentItem = NULL;
}

void TreeViewItem::appendChild(TreeViewItem *child)
{
    child->setParent(this);
    this->m_childItems.append(child);
}

void TreeViewItem::deleteAllChild()
{
    for (int index = 0; index < m_childItems.size(); index++)
    {
        m_childItems[index]->deleteAllChild();
    }
    qDeleteAll(m_childItems);
    m_childItems.clear();
}

TreeViewItem *TreeViewItem::child(int row)
{
    return m_childItems[row];
}

int TreeViewItem::childCount() const
{
    return m_childItems.size();
}

int TreeViewItem::columnCount() const
{
    return m_itemData.size();
}

QVariant TreeViewItem::data(int column) const
{
    return m_itemData[column];
}

void TreeViewItem::setParent(TreeViewItem *parent)
{
    m_parentItem = parent;
}

TreeViewItem *TreeViewItem::parent()
{
    return m_parentItem;
}

int TreeViewItem::row() const
{
    if (!m_parentItem) { return 0; }

    return m_parentItem->m_childItems.indexOf(const_cast<TreeViewItem*>(this));
}

TreeViewModel.h:

// TreeViewModel
#ifndef TREEVIEWMODEL_H
#define TREEVIEWMODEL_H

#include <QAbstractItemModel>
#include <QObject>
#include <QDebug>
#include "TreeViewItem.h"

#pragma execution_character_set("utf-8")

class TreeViewModel : public QAbstractItemModel
{
    Q_OBJECT
    enum ItemRoles {
        NAME = Qt::UserRole + 1,
        STATE
    };
public:
    TreeViewModel(const QStringList& headers, QObject *parent = NULL);
    ~TreeViewModel();

    // 必须要重写的五个函数
    Q_INVOKABLE QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE QModelIndex parent(const QModelIndex &child) const override;
    Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

    Q_INVOKABLE QHash<int, QByteArray> roleNames() const override;

private:
    TreeViewItem *itemFromIndex(const QModelIndex &index) const;

private:
    TreeViewItem *rootItem;
    QStringList m_headers;
};

#endif // TREEVIEWMODEL_H

TreeViewModel.cpp:

// TreeViewModel.cpp
#include "TreeViewModel.h"


// 返回给定父项和角色的项的索引
TreeViewModel::TreeViewModel(const QStringList& headers, QObject *parent)
{
    this->m_headers = headers;
    this->rootItem = new TreeViewItem(QVariantList());

    TreeViewItem *oneBook = new TreeViewItem(QVariantList() << "第一本书" << "None");
    oneBook->appendChild(new TreeViewItem(QVariantList() << "第一卷" << "None"));
    oneBook->child(0)->appendChild(new TreeViewItem(QVariantList() << "第一章" << "saved"));
    oneBook->child(0)->appendChild(new TreeViewItem(QVariantList() << "第二章" << "saved"));
    oneBook->child(0)->appendChild(new TreeViewItem(QVariantList() << "第三章" << "saving"));
    oneBook->appendChild(new TreeViewItem(QVariantList() << "第二卷" << "None"));
    oneBook->child(1)->appendChild(new TreeViewItem(QVariantList() << "第一章" << "saved"));
    oneBook->child(1)->appendChild(new TreeViewItem(QVariantList() << "第三章" << "no save"));

    TreeViewItem *twoBook = new TreeViewItem(QVariantList() << "第二本书" << "None");
    twoBook->appendChild(new TreeViewItem(QVariantList() << "第一卷" << "None"));
    twoBook->child(0)->appendChild(new TreeViewItem(QVariantList() << "第一章" << "saved"));
    twoBook->child(0)->appendChild(new TreeViewItem(QVariantList() << "第二章" << "saving"));
    twoBook->appendChild(new TreeViewItem(QVariantList() << "第二卷" << "None"));
    twoBook->child(1)->appendChild(new TreeViewItem(QVariantList() << "第一章" << "saved"));

    rootItem->appendChild(oneBook);
    rootItem->appendChild(twoBook);
}

TreeViewModel::~TreeViewModel()
{
    rootItem->deleteAllChild();
    rootItem = NULL;
}

// 在parent节点下,第row行,第column列位置上创建索引
QModelIndex TreeViewModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
    {
        return QModelIndex();
    }

    TreeViewItem *treeItem = itemFromIndex(parent);
    TreeViewItem *child = treeItem->child(row); // 第几行就是第几个子节点,TreeView一般都是一行一个节点的
    if (child) {
        return createIndex(row, column, child);
    } else {
        return QModelIndex();
    }

}

// 返回给定项目的父项目
QModelIndex TreeViewModel::parent(const QModelIndex &child) const
{
    if (!child.isValid())
    {
        return QModelIndex();
    }

    TreeViewItem *childItem = static_cast<TreeViewItem*>(child.internalPointer());
    TreeViewItem *parentItem = childItem->parent();

    if (parentItem == rootItem)
    {
        return QModelIndex();
    }

    return createIndex(parentItem->row(), 0, parentItem);
}

// 返回给定项目的行数
int TreeViewModel::rowCount(const QModelIndex &parent) const
{
    if (parent.column() > 0)
        return 0;

    TreeViewItem* item = itemFromIndex(parent);
    return item->childCount();
}

// 返回给定项目的列数
int TreeViewModel::columnCount(const QModelIndex &parent) const
{
    return 2;
    // 返回实际的列数 (实际是他返回了0,因为根节点用的是无参的构造),TreeView控件会认为表是空表,不获取数据
    if (parent.isValid())
    {
        return static_cast<TreeViewItem*>(parent.internalPointer())->columnCount();
    } else {
        return rootItem->columnCount();
    }
}

// 返回给定项目的指定角色数据
QVariant TreeViewModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    TreeViewItem *item = itemFromIndex(index);

    switch (role) {
    case NAME:
        return item->data(0);
    case STATE:
        return item->data(1);
    default:
        return QVariant();
    }

}

QHash<int, QByteArray> TreeViewModel::roleNames() const
{
    QHash<int, QByteArray> names(QAbstractItemModel::roleNames());
    names[NAME] = "name";
    names[STATE] = "state";
    return names;
}

// 获取父节点的数据指针,如果parent可用就用parent的节点,如果不可用就用总父节点
TreeViewItem *TreeViewModel::itemFromIndex(const QModelIndex &parent) const
{
    if (parent.isValid())
    {
        // internalPointer: 返回一个void *指针,用于模型将索引与内部数据结构关联起来
        TreeViewItem *item = static_cast<TreeViewItem*>(parent.internalPointer());
        return item;
    }
    return rootItem;
}

main.cpp:

// main.cpp
#include <QQmlContext>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "TreeViewModel.h"

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    // 把模型和qml对接的c++类暴露给qml
    TreeViewModel *model = new TreeViewModel(QStringList());
    engine.rootContext()->setContextProperty("myModel", model);

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

main.qml:

// main.qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.4

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


    Item {
        width: parent.width - 50
        height: parent.height - 50
        anchors.centerIn: parent

        TreeView {
            anchors.fill: parent
            model: myModel

            TableViewColumn {
                title: "Name"
                role: "name"
                width: 150
            }

            TableViewColumn {
                title: "State"
                role: "state"
                width: 150
                delegate: Text {
                    id: name
                    text: {
                        if(styleData.value === "None") {
                            qsTr("")
                        } else if (styleData.value === "saved") {
                            qsTr("")
                        } else if (styleData.value === "saving") {
                            qsTr("保存中……")
                        } else if (styleData.value === "no save") {
                            qsTr("未保存")
                        } else {
                            qsTr("")
                        }
                    }

                }
            }
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值