白金——挑战树状的model

依旧是先给出代码:

demo4

相信前面的例子给了你不少的信心,所以是时候挑战一下较难的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作为基类创建模板:

勾选最后一项,使用model自身的动态展开框架

我们可以看看生成的模板文件:

#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。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容