《QTreeView+QAbstractItemModel自定义模型》:系列教程之三[通俗易懂]

大家好,又见面了,我是你们的朋友全栈君。
本文属于 《QTreeView使用系列教程》 之一,欢迎查看其它文章。
1、了解常用的model类
通过对上一节的阅读,我们知道只要具备model+view就可以显示数据。
那么有哪些model类呢,从下图中我们可以看到

Qt中模型类的层次结构
QStandardItemModel:可以作为QListView、QTableView、QTreeView的标准model。
QAbstractListModel:需要使用QListView显示数据,并配合自定义model时,我们从此类继承。
QAbstractTableModel:需要使用QTableView显示数据时,并配合自定义model时,我们从此类继承。
QAbstractItemModel:需要使用QTreeView显示数据时,并配合自定义model时,我们从此类继承。
此处我们只关注可以用作QTreeView之model的类QAbstractItemModel与QStandardItemModel。
2、QStandardItemModel的使用
首先我们来看看如果用QStandardItemModel作为model时,我们的代码:
QTreeView* view = new QTreeView();
QStandardItemModel* model = new QStandardItemModel();
for (int row = 0; row < 4; ++row) {
QStandardItem *item = new QStandardItem(QString("%1").arg(row) );
model->appendRow( item );
view->setModel(model);
用法比较简单,QStandardItemModel可以使用QStandardItem,通过不断添加子节点,从而构建出list、table、tree结构的数据。
使用QStandardItemModel表示数据集具有以下优点:
- 实现代码简单
- 该类使用QStandardItem存放数据项,用户不必定义任何数据结构来存放数据项;
- QStandardItem使用自关联关系,能够表达列表、表格、树甚至更复杂的数据结构,能够涵盖各种各样的数据集;
- QStandardItem本身存放着多个『角色,数据子项』,视图类、委托类或者其他用户定义的类能够方便地依据角色访问各个数据子项。
缺点:
- 当数据集中的数据项很多时,施加在数据集上的某些操作的执行效率会很低。
- 数据太大时,占用内存巨大,性能低下
性能比较,可参考此文末尾的demo代码: https://blog.csdn.net/dpsying/article/details/80456263
3、QAbstractItemModel自定义model
(1)原理知识铺垫
- 我们以实现如下树形显示为例,进行自定义model。

我们要将数据显示到QTreeView中,按照Model/View框架介绍,需要定义2个类TreeModel和TreeItem,TreeModel继承于QAbstractItemModel,用于向View提供数据;
TreeItem用于定义我们的数据节点,然后被model获取数据。
QTreeView与TreeItem交互过程大致如下:

- 注意:在树中,我们一般默认认为,只有column为0的单元格才能添加下级单元格,也就是说树中的每一行单元格只能与Column为0的单元格建立父子关系。
所以我们可以简单的认为树,就是一行一行单元格组成的表格,只不过在每一行通过其首个单元格,建立了父子关系。
此处我们的一个TreeItem代表一行若干单元格,我们需要将多个TreeItem建立父子关系,就能够正确表示出树显示所需的数据结构。

- 而TreeItem的数据是从其他地方获取来的,所以我们先定义树中显示的原始数据结构,如下:
// person信息
typedef struct Person_t{
QString name; // 姓名
QString sex; // 性别
int age; // 年龄
QString phone; // 电话号码
Person_t()
age = 0;
} Person;
// 省份信息
typedef struct Province_t{
QString name;
QVector<Person*> people;
} Province;
(2)定义TreeItem类
- 提供建立树形结构的功能
通过addChild可以添加TreeItem子节点,并保存该子节点在父节点的序号。
void TreeItem::addChild(TreeItem *item)
item->setRow(_children.size());
_children.append(item);
}
另外提供释放子节点内存,用于删除根节点时,自动逐级释放所有TreeItem内存。
void TreeItem::removeChildren()
qDeleteAll(_children);
_children.clear();
}
返回父节点
TreeItem *parent() { return _parent; }
返回子节点数量
int childCount() const { return _children.count(); }
- 提供获取列数据,以及获取TreeItem子节点功能
既然TreeItem代表的是一行数据,那么必定需要提供获取某列数据函数。
QVariant data(int column) const;
也必定需要提供获取TreeItem下某子节点函数(某一行)。
TreeItem *child(int row) { return _children.value(row); }
- 关键:提供设置数据源地址功能
保存数据源地址,以便TreeItem可以访问原始数据;通常情况下,原始数据与TreeItem一一对应。
void setPtr(void* p) { _ptr = p; }
void* ptr() const { return _ptr; }
由于建立TreeItem对象树时,Province和Person地址会被setPtr()保存到TreeItem上,所以为了便于按类型取数据,在setPtr()时需要setType()保存数据属于哪种类型。
enum Type
UNKNOWN = -1,
PROVINCE,
PERSON
Type getType() const { return _type; }
void setType(const Type &value) { _type = value; }
到此,我们可以建立TreeItem树,并获取任意行、列的数据。已经满足了TreeModel获取任意数据的要求。
下一步,我们来定义TreeModel类。
(3)定义TreeModel类
我们需要继承自QAbstractItemModel,让我们来看看它有哪些接口。
QAbstractItemModel类中定义如下:
Q_INVOKABLE virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual QModelIndex parent(const QModelIndex &child) const = 0;
Q_INVOKABLE virtual QModelIndex sibling(int row, int column, const QModelIndex &idx) const;
Q_INVOKABLE virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
Q_INVOKABLE virtual bool hasChildren(const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
Q_INVOKABLE virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
Q_INVOKABLE virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
其中共5个纯虚函数,index()、parent()、rowCount()、columnCount()和data(),这是我们必须要实现的;另外一般我们还是需要显示表头的,所以还需要实现headerData()。QTreeView显示树时,会自动调用TreeModel,来获取显示一个树所需要的一些信息;我们重写这些函数的目的就是为了向QTreeView提供这些信息的。
接下来我们解释下重写各个函数的作用。
- QVariant headerData (int section, Qt::Orientation orientation, int role) const override;
表示获取表头数据,第section列;orientation方向,一般为水平方向;DisplayRole角色的表头数据,DisplayRole表示是用于界面显示的数据。
QStringList headers;
headers << QString("名称/姓名")
<< QString("性别")
<< QString("年龄")
<< QString("电话");
QVariant TreeModel::headerData(int section, Qt::Orientation orientation,int role) const
if (orientation == Qt::Horizontal)
if(role == Qt::DisplayRole)
return _headers.at(section);
return QVariant();
}
- int rowCount (const QModelIndex &parent) const override;
获取索引parent下有多少行。View会遍历每个单元格索引,若不是第一列单元格索引,则不会有子节点,所以直接返回行数为0;
若是第一列单元格索引,那么该单元格是否为空(空表示根节点),则需要返回根节点下行数,反之则返回parent下行数。
int TreeModel::rowCount(const QModelIndex &parent) const
if (parent.column() > 0)
return 0;
TreeItem* item = itemFromIndex(parent);
return item->childCount();
}
- int columnCount (const QModelIndex &parent) const override;
返回索引parent下有多少列
int TreeModel::columnCount(const QModelIndex &parent) const
return _headers.size();
}
- QModelIndex index (int row, int column, const QModelIndex &parent) const override;
在parent节点下,第row行,第column列位置上创建索引;将TreeItem指针保存至该索引。
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
if (!hasIndex(row, column, parent))
return QModelIndex();
TreeItem *parentItem = itemFromIndex(parent);
TreeItem *item = parentItem->child(row);
if (item)
return createIndex(row, column, item);
return QModelIndex();
}
- QVariant data (const QModelIndex &index, int role) const override;
获取index.row行,index.column列数据;通过itemFromIndex()获取保存在索引index中的TreeItem指针。
QVariant TreeModel::data(const QModelIndex &index, int role) const
if (!index.isValid())
return QVariant();
TreeItem *item = itemFromIndex(index);
if (role == Qt::DisplayRole)
return item->data(index.column());
return QVariant();
}
- QModelIndex parent (const QModelIndex &index) const override;
创建index的父索引,若父节点为根节点,则返回QModelIndex(),默认根节点索引为空。
QModelIndex TreeModel::parent(const QModelIndex &index) const
if (!index.isValid())
return QModelIndex();
TreeItem *item = itemFromIndex(index);
TreeItem *parentItem = item->parent();
if (parentItem == _rootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
TreeModel类一般不需要怎么修改,都大同小异,实际使用时,根据需要微调就可以。
4、测试TreeModel
初始化原始数据:
QVector<Province*> MainWindow::initData()
// 初始化数据,5个省,每个省5人
QVector<Province*> proList;
int provinceCount = 5;
int personCount = 5;
for(int i = 0; i < provinceCount; i++)
Province* pro = new Province();
pro->name = QString("Province%1").arg(i);
for(int j = 0; j < personCount; j++)
Person* per = new Person();
per->name = QString("name%1").arg(j);
per->sex = "man";
per->age = 25;
per->phone = "123456789";
pro->people.append(per);
proList.append(pro);
return proList;
}
构建TreeItem对象树,设置model:
void MainWindow::setModel(const QVector<Province *> &proList)
QStringList headers;
headers << QString("名称/姓名")
<< QString("性别")
<< QString("年龄")
<< QString("电话");
TreeModel* model = new TreeModel(headers, treeView);
TreeItem* root = model->root();
foreach (auto pro, proList)
TreeItem* province = new TreeItem(root);
province->setPtr(pro); // 保存数据指针
province->setType(TreeItem::PROVINCE); // 设置节点类型为PROVINCE
root->addChild(province);
foreach (auto per, pro->people)
TreeItem* person = new TreeItem(province);
person->setPtr(per); // 保存数据指针
person->setType(TreeItem::PERSON); // 设置节点类型为PERSON