依旧是先给出代码:
相信前面的例子给了你不少的信心,所以是时候挑战一下较难的model了,就想QFileSystemModel一样,我们是不是也能实现一个树状的model呢?
为了降低树状model的难度,我们在这个例子中不考虑阻塞ui的问题和监听目录的问题,只是单纯的做出一个支持树状结构的model。
考虑到QFileSystemModel的复杂性,我们在自己的Model中可能不能完全照搬它提供的接口,不够我们至少要能够指定定model的根目录,这样才能够进行跳转的操作。
有了上一章的知识积累,我们可以直接从数据类型的构建开始。
我们在脑海中应该能够构建出一个文件系统的树状结构——每一个文件都是一个节点,对于文件夹或者挂载点而言,他们除了本身信息和它们的parent的信息,还有他们的子项的信息。我们把文件、挂载点和文件夹的特性去并集进行抽象能够得到类似这样的一个类
#伪代码
class FileNode
{
public:
FileNode(const QString &uri, FileNode *parentNode);
FileInfo getInfo();
const FileNode *getParent();
const List<FileNode*> getChildren();
private:
FileNode *m_parent;
FileInfo *m_info;
List<FileNode*> m_children;
}
通过这样的数据结构,我们就能够以递归的形式展示一个文件系统。然而我们还看不出它和model如何结合起来,我们接着往下看。
创建基于QAbstractItemModel的模板
要创建一个树状的model,显然之前一章的model是不够看的了,我们需要使用QAbstractItemModel作为基类创建模板:
我们可以看看生成的模板文件:
#ifndef SIMPLEGVFSTREEMODEL_H
#define SIMPLEGVFSTREEMODEL_H
#include <QAbstractItemModel>
class SimpleGVfsTreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit SimpleGVfsTreeModel(QObject *parent = nullptr);
// Header:
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// Basic functionality:
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;
// Fetch data dynamically:
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private:
};
#endif // SIMPLEGVFSTREEMODEL_H
可以发现,这个类比起之前的例子多出很多东西,说明之前的model已经帮我们做了很多的准备工作,而我们现在的model这些工作都必须要自己去实现才能达到理想的效果。
我们注意到parent和index这两个方法,这两个方法构成了树状model的基本索引框架,保证了数据的双向可检索性;
hasChildren方法在树状图中表示是否可以展开,即view item左侧是否有一个指示箭头;
canFetchMore方法表示是否可以获取更多,如果一个item的hasChildren被触发,canFetchMore也会被触发,这一般发生在展开一个item或者滚动滚轮的时候。
fetchMore是qtmodel预留的执行动态增加数据的入口,如果canFetchMore返回true,那么它会被调用,一个动态获取数据的model可能会执行很多次fetchMore,直到所有parent的canFetchMore返回均为false为止。
为了和model匹配,我们的FileNode类需要做如下改进:
#伪代码
class FileNode
{
friend class Model; //使得Model能够访问和操作FileNode的私有成员
public:
//FileNode(const QString &uri, FileNode *parentNode);
FileNode(const QString &uri, FileNode *parentNode, Model *model); //增加Model构造参数
FileInfo getInfo();
const FileNode *getParent();
const List getChildren();
//改进
const QModelIndex firstColumnIndex();
bool hasChildren(); //TODO:如果是文件夹或者可挂载节点,则为true
bool canFetchMore(); //TODO:没有进行fetchMore,则为true
void fetchMore(); //TODO:对于文件夹,进行遍历;对于挂载点,如果挂载,对其target进行遍历
private:
FileNode *m_parent;
FileInfo *m_info;
List m_children;
Model *m_model;
}
这样我们就能够使用model提供的动态获取接口了,注意firstColumnIndex()这个方法,单独的FileNode类是没有意义的,在上一个例子中,我们直接吧文件节点放在了model里,对于这个复杂的例子而言显然这样做不太合适,然而我们依然希望item能够提供一个快速对应到model index 的方法,这对于树状的model是十分重要的,然而这个model还没有被使用过,获取index的方法也是一样,我们现在还看不太出头绪,我们接下去看。
树状model下的index方法
想要解决上面遗留下来的疑惑,我们必须弄清树状model的index方法的实现
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
index方法需要返回一个QModelIndex,而这个方法本身就是为了获取item的index而存在的——所以,index是生成item index的方法。qt官方文档对该方法有如下介绍:
Returns the index of the item in the model specified by the given row, column and parent index.
When reimplementing this function in a subclass, call createIndex() to generate model indexes that other components can use to refer to items in your model.
Note: This function can be invoked via the meta-object system and from QML. See Q_INVOKABLE.
See also createIndex().
它能够通过createIndex()构建一个QModelIndex,我们看看createIndex()这个方法:
QModelIndex QAbstractItemModel::createIndex(int row, int column, void *ptr = nullptr) const
Creates a model index for the given row and column with the internal pointer ptr.
When using a QSortFilterProxyModel, its indexes have their own internal pointer. It is not advisable to access this internal pointer outside of the model. Use the data() function instead.
This function provides a consistent interface that model subclasses must use to create model indexes.
在QModelIndex中,有这样一个方法与之对应:
void *QModelIndex::internalPointer() const
Returns a void * pointer used by the model to associate the index with the internal data structure.
See also QAbstractItemModel::createIndex().
createIndex中传入的ptr(internal pointer)实际上就是我们的FileNode,通过这种方式,我们的数据节点就和index有了一一对应的关系,这样上面的疑惑就解决了。在createIndex的时候,实际上我们就已经完成了绑定,需要注意的是,对于相同parent和row但column不同的index的创建,我们往往都直接把FileNode的地址赋值给index的internal pointer,而没有必要针对不同的column提供不同的地址。
parent与index的协同
仅仅只有index接口是不够的,我们还需要parent()作为辅助才能够完成filenodes向model的映射。我们下面结合着代码来看他们究竟是如何协作的。
QModelIndex SimpleGVfsTreeModel::index(int row, int column, const QModelIndex &parent) const
{
//qDebug()<<"index()"<<parent<<row;
// FIXME: Implement me!
if (!parent.isValid()) {
return createIndex(row, column, m_root_node->m_children.at(row));
} else {
FileNode *parentNode = static_cast<FileNode*>(parent.internalPointer());
return createIndex(row, column, parentNode->m_children.at(row));
}
}
QModelIndex SimpleGVfsTreeModel::parent(const QModelIndex &index) const
{
//qDebug()<<"parent()"<<index;
// FIXME: Implement me!
FileNode *childNode = static_cast<FileNode*>(index.internalPointer());
if (childNode->getParentNode())
return childNode->getParentNode()->firstColumnIndex();
return QModelIndex();
}
在index()方法中,我们通过判断parent是否合法在createIndex中填入对应的FileNode。
我们可以看到,在parent()方法中,我们传入的parent必定是一个带有FileNode的index,所以我们能够获取这个FileNode的firstColumnIndex作为这个index的parentIndex。
我们再看看firstColumnIndex是怎么实现的:
QModelIndex FileNode::firstColumnIndex()
{
return m_model->firstColumnIndex(this);
}
QModelIndex SimpleGVfsTreeModel::firstColumnIndex(FileNode *node)
{
if (node->getParentNode()) {
int row = node->getParentNode()->m_children.indexOf(node);
return createIndex(row, 0, node);
} else {
int row = this->m_root_node->m_children.indexOf(node);
return createIndex(row, 0, node);
}
return QModelIndex();
}
它实际上还是要通过Model的createIndex方法去创建一个带有指向该FileNode的指针的index,需要注意的是,createIndex所创建出来的index本身并没有parent这一概念,它需要index指向的FileNode的协助才能够找到够形成逻辑上的树状结构,我们通过index() parent() 和 firstColumnIndex()的协作,实现了FileNode向Model的映射。
另外,我们的index传入的另外两个参数row和column其实上受到了rowCount()和columnCount()的影响,row永远在0到对应parentIndex的rowCount-1这个区间内,column同理,这实际上也是我们看不到的model处理数据的内部过程实现的。
fetchMore()并不好用
我其实很少使用qt model提供的动态获取数据的接口,它有几点不足的地方:
首先,如果需要处理大量的数据,在fetchMore()中进行处理会造成ui的卡顿,如果使用异步处理,那么判断是否可以fetchMore又会变得更加复杂,如果处理不当可能会出现重复的fetchMore。
其次,fetchMore之后的数据不会自动释放,如果我们关闭一个展开的row,它任然会保存数据,这对于资源来说可能是一种浪费。
我一般采用connect QTreeView的展开和收缩信号的方式与model的insertRow(s)和removeRow(s)实现fetchMore的效果,因为它可以解决上面的两个问题。在这里我不给出代码,我希望你能够自己基于demo4的代码进行修改实现这种方式,这也是为了考验你是否理解了model/view编程所留下的作业。
结合demo2
我们知道demo2增加了路径跳转和右键菜单,那么我们的demo4该怎么做呢?我希望你能够将这些demo中的知识融会贯通,实现一个更完善的demo。