我们先给出代码:
准备工作
从前面章节我们知道,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的模板,我们在新建一个类型的时候可以从合适的模板进行创建。
这样,我们就快速的搭建起了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回收或者还原一个文件试试看动态改变的效果。