黄金——实现一个用于展示回收站文件的model

我们先给出代码:

demo3

准备工作

从前面章节我们知道,QFileSystemModel无法表示回收站这个虚拟文件系统的域,其实是因为它的数据核心QFileInfo并不支持虚拟文件系统这一概念,如果我们需要使用model/view展示虚拟文件系统,我们需要引入第三方的库GLib,首先我们要配置它的开发环境:

sudo apt install libglib2.0-dev

在安装完成之后我们可以使用gio提供的测试性质的二进制文件gio-tools来验证我们回收站内的文件:

gio list trash:///

如果回收站内没有文件,那么首先我们可以用文件管理器选中一些文件然后删除他们(注意不是彻底删除),或者使用

gio trash 某文件名

先回收文件,在查看。另外,如果你想实时的监听回收站的变化,你可以另起一个终端执行

gio monitor trash:///

在进程运行期间,回收站发生的改变会即刻打印到该终端内。

我们要实现一个回收站文件的展示,上面的部分还不够,只是验证了技术的可行性,接下来我们需要分析一下需求,首先我们回收站的目录结构应该是只有一层的,那么我们就不需要像QFileSystemModel那样支持树状结构的显示,在我们的回收站发生改变时我们可能需要对数据进行同步。

相信大家心里也有一个底了,我们现在来开始动手实现这样一个Model。

首先,我们需要创建一个model类,作为qt重要的一环,qtcreator提供了各种model的模板,我们在新建一个类型的时候可以从合适的模板进行创建。


新建文件中选择Qt=>Qt Item Model


选择符合条件的模板

这样,我们就快速的搭建起了model的框架,由于model的概念相对难以理解,这对于刚刚接触这个概念的同学是一个福音,你可以创建不同的model模板,并且比较一下他们的异同,然后你应该能发现,随着模板model的需求和功能越来越明确,它需要重写的接口就越来越少,可扩展性也越来越低。所有的model模板乃至其它的model的基类最终都是QAbstractItemModel,它具有最强的扩展性,相对的需要实现的接口也更多。

其次,为了在qt开发中使用glib,我们需要对我们这个项目的pro文件进行修改

将原来的“CONFIG += c++11”这一行替换成

CONFIG += c++11 link_pkgconfig no_keywords
PKGCONFIG += glib-2.0 gio-2.0

需要注意,no_keywords会取消qt对信号和槽的宏定义signals和slots,如果要定义信号和槽,要使用Q_SIGNALS和Q_SLOTS进行声明。之所以要加这个no_keywords的原因是因为glib中的定义重名了。

这样我们就配置好了开发环境,下面就要直接开始进行开发了。

使用gio获取回收站内的文件信息

我们首先需要获取回收站文件的信息,为了获取回收站文件信息,我们需要使用gio的接口GFileEnumerator去遍历和query。

定义如下方法:

const QStringList enumerateTrash()
{
    QStringList uris;
    GFile *gfile = g_file_new_for_uri("trash:///");
    GFileEnumerator *g_enumerator = g_file_enumerate_children(gfile,
                                                              G_FILE_ATTRIBUTE_STANDARD_NAME,
                                                              G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                                              nullptr,
                                                              nullptr);
    GFileInfo *ginfo = g_file_enumerator_next_file(g_enumerator,
                                                   nullptr,
                                                   nullptr);
    while (ginfo) {
        GFile *file = g_file_enumerator_get_child(g_enumerator, ginfo);
        char *uri = g_file_get_uri(file);
        uris<<uri; //add file uri to list.
        g_free(uri);
        g_object_unref(file);
        g_object_unref(ginfo);

        ginfo = g_file_enumerator_next_file(g_enumerator,
                                            nullptr,
                                            nullptr); //next file
    }

    return uris;
}

每当我们调用这个方法,它都会返回当前回收站中所有文件的uri list。

数据结构的设计

我们可以从新建的模板中发现,model中没有定义数据的结构,这说明model和数据的关系并不是完全等价的,我们首先设计好数据的类型,然后再看它们是如何绑定在一起的。

定义一个TrashVFSFileItem类

#ifndef TRASHVFSFILEITEM_H
#define TRASHVFSFILEITEM_H

#include <QObject>
#include <QIcon>

class TrashVFSFileItem : public QObject
{
    Q_OBJECT
public:
    explicit TrashVFSFileItem(const QString &uri, QObject *parent = nullptr);
    const QString getUri() {return m_uri;}
    const QString getDisplayName() {return m_display_name;}
    const QIcon getIcon() {return m_icon;}

private:
    QString m_uri;
    QIcon m_icon;
    QString m_display_name;
};

#endif // TRASHVFSFILEITEM_H

#include "trashvfsfileitem.h"

#include <gio/gio.h>
#include <QFileIconProvider>

TrashVFSFileItem::TrashVFSFileItem(const QString &uri, QObject *parent) : QObject(parent)
{
    m_uri = uri;
    GFile *gfile = g_file_new_for_uri(uri.toUtf8().constData());
    GFileInfo *ginfo = g_file_query_info(gfile,
                                         G_FILE_ATTRIBUTE_STANDARD_NAME,
                                         G_FILE_QUERY_INFO_NONE,
                                         nullptr,
                                         nullptr);
    const char *display_name = g_file_info_get_display_name(ginfo);
    m_display_name = display_name;

    GFileType type = g_file_query_file_type(gfile, G_FILE_QUERY_INFO_NONE, nullptr);
    QFileIconProvider p;
    if (type == G_FILE_TYPE_DIRECTORY) {
        m_icon = p.icon(QFileIconProvider::Folder);
    } else {
        m_icon = p.icon(QFileIconProvider::File);
    }

    g_object_unref(ginfo);
    g_object_unref(gfile);
}

我们的回收站文件类有3个私有成员,uri、图标和显示名称,uri主要是为了之后动态监听回收站变化所需要的匹配项,二图标和显示名称是一个视图所需要的最基本的两个数据项。

数据的呈现

在QAbstractListModel中,关于数据呈现的待实现方法只有两个:

int rowCount(const QModelIndex &parent = QModelIndex()) const override;

QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

rowCount()告诉view需要绘制多少元素。值得注意的一点是,QAbstractListModel的rowCount已经有了缺省参数QModelIndex(),在model的设计中,我们把QModelIndex()认为是非法的index,而parent是非法的index往往代表者它是处于视图顶层的index。

其实关于呈现数据的接口还有两个,他们在QAbstractListModel中已经被实现了:

QModelIndex parent(const QModelIndex &index) const;

int columnCount(const QModelIndex &parent = QModelIndex()) const;

由于QAbstractListModel是list结构,所以所有的view item对应的index都为QModelIndex(),columnCount()永远返回1。我们也可以推敲出QAbstractTableModel的parent()也永远返回QModelIndex();而树状的model则需要实现这四个方法,使其相互配合才能够实现。

data告诉view绘制的内容,它是由index和role共同决定的,在QAbstractListModel中,index只与row有关,每一个row又对应了我们的数据载体m_children的item;role顾名思义是绘制的角色,最常用的角色有三种——装潢、显示名称和tool tip,这些也都是在之前的数据结构中保存下来了的。我们看一下两个方法具体的定义:

int TrashFilesListModel1::rowCount(const QModelIndex &parent) const
{
    // For list models only the root node (an invalid parent) should return the list's size. For all
    // other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
    if (parent.isValid())
        return 0;

    // FIXME: Implement me!
    return m_children.count();//return the list count
}

QVariant TrashFilesListModel1::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    // FIXME: Implement me!
    switch (role) {
    case Qt::DecorationRole:
        return m_children.at(index.row())->getIcon();
    case Qt::DisplayRole:
        return m_children.at(index.row())->getDisplayName();
    case Qt::ToolTipRole:
        return m_children.at(index.row())->getDisplayName();
    default:
        break;
    }
    return QVariant();
}

这样,我们的数据就和model、和view联系起来了。

监听回收站的改变与信号的发送

我们希望在运行的时候,我们的视图能够动态响应回收站的变化,显然上面的工作还做不到这一点;为了实现这样的功能,我们需要使用gio的接口对回收站的改变进行监听,并且转化为qt的信号。在本例中,这个操作可以直接放在我们的model的初始化中:

TrashFilesListModel1::TrashFilesListModel1(QObject *parent)
    : QAbstractListModel(parent)
{
    QStringList uris = enumerateTrash();
    for (auto uri : uris) {
        TrashVFSFileItem *item = new TrashVFSFileItem(uri);
        m_children.append(item);
    }

    GFile *trash = g_file_new_for_uri("trash:///");
    trash_monitor = g_file_monitor_directory(trash,
                                             G_FILE_MONITOR_SEND_MOVED,
                                             nullptr,
                                             nullptr);

    signal_handler = g_signal_connect(trash_monitor, "changed", GCallback(direcoty_changed_cb), this);

    connect(this, &TrashFilesListModel1::itemAdded, [=](const QString &uri){
        TrashVFSFileItem *newItem = new TrashVFSFileItem(uri);
        m_children.append(newItem);
        this->insertRow(m_children.indexOf(newItem), QModelIndex());//trigger the view change.
    });

    connect(this, &TrashFilesListModel1::itemRemoved, [=](const QString &uri){
        for (auto child : m_children) {
            if (child->getUri() == uri) {
                this->removeRow(m_children.indexOf(child), QModelIndex());//trigger the view change.
                return;
            }
        }
    });
}

可以看到我们连接了一个glib的信号,当回收站内容改变时,程序进入回调方法,然后在回调方法中再提交一次itemAdded或者itemDeleted信号。我们的回调函数是这样写的:

GCallback TrashFilesListModel1::direcoty_changed_cb(GFileMonitor *monitor,
                                                    GFile *file,
                                                    GFile *other_file,
                                                    GFileMonitorEvent event_type,
                                                    gpointer user_data)
{
    TrashFilesListModel1 *p_this = static_cast<TrashFilesListModel1*>(user_data);
    char *changed_uri = g_file_get_uri(file);
    QString uri = changed_uri;
    g_free(changed_uri);
    switch (event_type) {
    case G_FILE_MONITOR_EVENT_CREATED:
        Q_EMIT p_this->itemAdded(uri);
        break;
    case G_FILE_MONITOR_EVENT_DELETED:
        Q_EMIT p_this->itemRemoved(uri);
        break;
    default:
        break;
    }
}

注意:由于glib的信号触发是回调机制的,需要将回调方法标为static,而static方法不能够直接进行qt信号的emit,所以我们需要在g_signal_connect时设置传入的user_data为TrashFilesListModel1本身,这样我们才能够在callback中进行qt信号的提交——这可以算是glib信号向qt信号转化的标准封装形式。

响应信号

在信号提交后,我们应该开始对Model进行修改,然后通知视图进行改变。在这里,model通知视图的改变是通过insertRow或者removeRow实现的,当内部的beginXXXModel()执行后,view的事件会被阻塞,当其内部的endXXXModel()执行后,view马上就会刷新并重绘。

细心的同学可能发现,从我的提供的代码里面,响应itemAdded和itemDeleted的流程有所区别。我们必须谨慎考虑insert/remove事件的响应时机,我们对视图的其它操作,比如缩放也有可能对view进行刷新,从而重新触发model的交互流程,如果这个时候操作不当,会带来一些不利的影响,这个不利的影响我会在分析Qt model的排序和筛选框架时举例分析。

结果:


回收站内有两个文件

Good job,现在你可以使用文件管理器或者gio-tools回收或者还原一个文件试试看动态改变的效果。

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

推荐阅读更多精彩内容