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