白金——挑战树状的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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容