QT5.14.2自带Examples:Simple Tree Model

功能概述

本示例展示了如何将层次模型与Qt的标准视图类一起使用。
在这里插入图片描述
Qt的model/view体系结构为视图操作数据源中的信息提供了一种标准方法,使用数据的抽象模型来简化和标准化访数据的访问。简单模型将数据表示为表格项,并允许视图通过基于索引的系统访问这些数据。通过允许每个项充当子项表的父项,可以使用模型以树结构的形式表示数据。
在尝试实现树模型之前,应该考虑数据是由外部源提供的,还是将在模型本身中维护。

设计

我们采用一个或多个TreeItem对象组成的树结构作为数据结构。每一个TreeItem表示树视图中的一个项,并且包含了数据的多个列。
存储在模型内部的数据使用一个或多个TreeItem对象,通过基于指针的树结构相互关联。通常,每个TreeItem都有一个父项,并且可以有许多子项。但是,树结构中的根项没有父项,并且它从未在模型外部被引用。每个TreeItem都包含关于它在树结构中的位置的信息;它可以返回其父项及其行号。这些信息的方面获取,使模型的实现更容易。
由于树视图中的每个项通常包含几列数据(本例中为标题和摘要),因此将这些信息存储在每个项中是很自然的选择。为了简单起见,我们将使用QVariant对象列表来存储项中每个列的数据。
在这里插入图片描述
使用基于指针的树结构意味着,当向视图传递模型索引时,我们可以在索引中记录相应项的地址(参见QAbstracteModel::createIndex()),然后使用QModelIndex::internalPointer()检索它。这使得编写模型更容易,并确保引用同一项数据的所有模型索引都具有相同的内部数据指针。有了适当的数据结构,我们可以用最少的额外代码创建树模型,以向其他组件提供模型索引和数据。

程序结构

为了实现tree的容器(数据结构实现),我们需要先实现tree node的容器,如下图:
在这里插入图片描述

TreeItem 类定义

#ifndef TREEITEM_H
#define TREEITEM_H
#include <QVariant>
#include <QVector>

class TreeItem
{
public:
	//TreeItem为节点的类型,对象可能是根,也可能是叶子,或是普通节点。
    explicit TreeItem(const QVector<QVariant> &data, TreeItem *parentItem = nullptr);
    ~TreeItem();
	// 用于在首次构造模型时添加数据,本例不能修改数据,所以在正常使用期间不被使用。
    void appendChild(TreeItem *child);
	//child()和childCount()函数允许模型获取有关任何子项的信息。
    TreeItem *child(int row);
    int childCount() const;
    //与项关联的列数信息由columnCount()提供,可以使用data()函数获取每列中的数据。
    int columnCount() const;
    QVariant data(int column) const;
    //row()和parentItem()函数用于获取项的行号和父项。
    int row() const;
    TreeItem *parentItem();

private:
	// 包含子项指针列表
    QVector<TreeItem*> m_childItems;
    // 数据
    QVector<QVariant> m_itemData;
    //父节点指针
    TreeItem *m_parentItem;
};
#endif // TREEITEM_H

该类是一个普通的C++类。它不从QObject继承,也不提供信号和槽。它使用QVariants列表保存数据,其中包含列数据,以及其在树结构中位置的信息。
下图中右边为程序运行时,数据加载完毕后的数据结构:
在这里插入图片描述

TreeItem 类实现

//tree模型数据项的容器(tree node)。
#include "treeitem.h"
//构造函数仅用于记录项的父级,以及与每个列关联的数据。
TreeItem::TreeItem(const QVector<QVariant> &data, TreeItem *parent)
    : m_itemData(data), m_parentItem(parent)
{}

TreeItem::~TreeItem()
{
	//析构时,回收内存。
    qDeleteAll(m_childItems);
}
//每个子项都是在模型初始化都是会后填充的数据,因此添加子项的函数很简单
void TreeItem::appendChild(TreeItem *item)
{
    m_childItems.append(item);
}
//返回子项列表中与指定行号对应的子项
TreeItem *TreeItem::child(int row)
{
    if (row < 0 || row >= m_childItems.size())
        return nullptr;
    return m_childItems.at(row);
}
//TreeModel使用此函数确定给定父项的行数。
int TreeItem::childCount() const
{
    return m_childItems.count();
}

int TreeItem::columnCount() const
{
    return m_itemData.count();
}

QVariant TreeItem::data(int column) const
{
    if (column < 0 || column >= m_itemData.size())
        return QVariant();
    return m_itemData.at(column);
}
//注意,由于模型中的根项没有父项,在这种情况下,此函数将返回0。
TreeItem *TreeItem::parentItem()
{
    return m_parentItem;
}
//报告项在其父项列表中的位置
int TreeItem::row() const
{
    if (m_parentItem)
        return m_parentItem->m_childItems.indexOf(const_cast<TreeItem*>(this));

    return 0;
}

TreeModel类定义

tree node的容器准备好了,现在就可以在TreeModel中构建tree的数据结构了。

#ifndef TREEMODEL_H
#define TREEMODEL_H

#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>

class TreeItem;
//QAbstractItemModel类定义了model/view体系结构中项模型必须使用的标准接口
class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
	//这里的data是整个tree的数据
    explicit TreeModel(const QString &data, QObject *parent = nullptr);
    ~TreeModel();
	//通过索引获取数据项
    QVariant data(const QModelIndex &index, int role) const override;
    //item的属性设置,是否可以编辑,是否可拖拽等,具体可以看它的枚举量说明。
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override;
    QModelIndex index(int row, int column,
                      const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

private:
	//通过给定的数据,建立tree
    void setupModelData(const QStringList &lines, TreeItem *parent);

    TreeItem *rootItem;
};

#endif // TREEMODEL_H

这个类类似于 QAbstractItemModel 的大多数提供只读模型的子类。只是构造函数和setupModelData()函数的形式有所不同。

TreeModel类实现

//提供一个简单的树模型,以显示如何创建和使用层次模型。
#include "treemodel.h"
#include "treeitem.h"
#include <QStringList>
//为简单起见,模型数据不允许被编辑。因此,构造函数接受一个参数,该参数包含模型将与视图和代理共享的数据。

TreeModel::TreeModel(const QString &data, QObject *parent)
    : QAbstractItemModel(parent)
{
	//由构造函数为模型创建根项(仅包含标题数据)
	//注意{}符号,这是一个数据。
    rootItem = new TreeItem({tr("Title"), tr("Summary")});
    //为模型填充数据,data为文件读取的文本数据,通过换行符切分
    //每行数据是rootItem的一个row数据
    setupModelData(data.split('\n'), rootItem);
}

TreeModel::~TreeModel()
{
    delete rootItem;
}

int TreeModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
    //internalPointer返回模型用于将索引与内部数据结构关联的void*指针。
    //也就是通过parent索引,找到数据的指针。
        return static_cast<TreeItem*>(parent.internalPointer())->columnCount();
    return rootItem->columnCount();
}

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (role != Qt::DisplayRole)
        return QVariant();
	//通过索引找到数据的指针。
    TreeItem *item = static_cast<TreeItem*>(index.internalPointer());

    return item->data(index.column());
}

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

    return QAbstractItemModel::flags(index);
}

QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
                               int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
        return rootItem->data(section);

    return QVariant();
}
//模型必须实现index()函数给视图或代理使用。
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return QModelIndex();

    TreeItem *parentItem;
	//如果无效,我们就假设要找的是根
    if (!parent.isValid())
        parentItem = rootItem;
    else
    //用它的internalPointer()函数从模型索引中获取数据指针,并用它引用TreeItem对象
    //我们构造的所有模型索引,都将包含指向现有TreeItem的指针
        parentItem = static_cast<TreeItem*>(parent.internalPointer());

    TreeItem *childItem = parentItem->child(row);
    if (childItem)
    //函数输入的最后一个参数是索引,这里返回的最后一个参数是对象指针
    //也就是为内部对象,创建一个索引。
    //对象指针在model里我们通常称为内部指针,只在model内部使用。外部调用时,应该使用data函数
    //所以我们的对外函数,参数都采用index,internalPointer只在函数内部使用。
    // return createIndex(childItem->row(), 0, childItem);
        return createIndex(row, column, childItem);
    return QModelIndex();
}
//我们对外的返回值,也都是索引,而不是内部指针。
//传入一个节点的索引,返回它的父节点的索引。
QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();
	//通过索引找到内部对着指针。
    TreeItem *childItem = static_cast<TreeItem*>(index.internalPointer());
    //找到父节点的内部对象指针。
    TreeItem *parentItem = childItem->parentItem();

    if (parentItem == rootItem)
        return QModelIndex();
	//如果父节点不是根节点,那么返回parentItem所对应的索引。
    return createIndex(parentItem->row(), 0, parentItem);
}

int TreeModel::rowCount(const QModelIndex &parent) const
{
    TreeItem *parentItem;
    if (parent.column() > 0)
        return 0;

    if (!parent.isValid())
        parentItem = rootItem;
    else
        parentItem = static_cast<TreeItem*>(parent.internalPointer());
	//输入的是索引,通过索引找到数据的内部指针,调用数据的函数获取值。
    return parentItem->childCount();
}

void TreeModel::setupModelData(const QStringList &lines, TreeItem *parent)
{
	//通过压栈,出栈的方式创建tree结构
	//父节点(栈)
    QVector<TreeItem*> parents;
    //缩进(栈)
    QVector<int> indentations;
    //压栈(初始状态:父节点为根,缩进为0)
    parents << parent;
    indentations << 0;
	//行数:第几行
    int number = 0;

    while (number < lines.count()) {
        int position = 0;
        while (position < lines[number].length()) {
        //如果有空格,记录空格的个数。否则退出循环。
        //用于判断缩进,如果空格个数大于indentations栈中的最后一个值,表示需要创子节点。需要压栈,反之则需要1-n次出栈(新创建的节点为当前的父级,或父级的父级或... ...)	
            if (lines[number].at(position) != ' ')
                break;
            position++;
        }
        //mid:返回一个字符串,从指定的位置索引开始。实际上本例不需要,因为前面是空格,会被strimmed掉。
        //trimmed:返回从开头和结尾删除空白的字符串。
        const QString lineData = lines[number].mid(position).trimmed();

        if (!lineData.isEmpty()) {
            const QStringList columnStrings = lineData.split('\t', QString::SkipEmptyParts);
            QVector<QVariant> columnData;
            // 提高效率,避免内存碎片。否则容器增加内容时,会触发重新分配内存。
            columnData.reserve(columnStrings.count());
            for (const QString &columnString : columnStrings)
                columnData << columnString;
			//空格比上一次压栈的值更多,需要压栈
            if (position > indentations.last()) {
                //先判断,当前的parent是否有子节点。防止跨级。
                if (parents.last()->childCount() > 0) {
                    //压栈操作
                    parents << parents.last()->child(parents.last()->childCount()-1);
                    indentations << position;
                }
            } else {
                //如果发现反方向的缩进,则进行出栈操作。改变当前的parent和indentation值
                //注意这里用的是while,不是if,1到n次出栈。
                while (position < indentations.last() && parents.count() > 0) {
                    parents.pop_back();
                    indentations.pop_back();
                }
            }

            // 将新项附加到当前父级的子级列表中。
            parents.last()->appendChild(new TreeItem(columnData, parents.last()));
        }
        ++number;
    }
}


数据加载完毕后的数据结构:
在这里插入图片描述

main 函数

#include "treemodel.h"

#include <QApplication>
#include <QFile>
#include <QTreeView>

int main(int argc, char *argv[])
{
    Q_INIT_RESOURCE(simpletreemodel);

    QApplication app(argc, argv);

    QFile file(":/default.txt");
    file.open(QIODevice::ReadOnly);
    TreeModel model(file.readAll());
    file.close();

    QTreeView view;
    view.setModel(&model);
    view.setWindowTitle(QObject::tr("Simple Tree Model"));
    view.show();
    return app.exec();
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值