10. Model/View

Model/View直译过来就是模式和视图,Qt中叫模型/视图体系结构。

模型-视图-控制器(MVC)模式是一种源自Smalltalk的设计模式,《设计模式》一书中有这样一句:

MVC由三种对象组成。模型是应用程序对象,视图是其屏幕显示,控制器定义了用户界面对用户输入的反应方式。 在MVC之前,用户界面设计倾向于将这些对象放在一起。MVC使它们解耦以增加灵活性和重用性。

参考 深入理解MVC

如果将视图和控制器对象组合在一起,则结果将是模型/视图架构。这仍然将存储数据的方式与将数据呈现给用户的方式分开, 但是基于相同的原理提供了一个更简单的框架。这种分离使得可以在几个不同的视图中显示相同的数据,并实现新的视图类型,而无需更改基础数据结构。 为了允许灵活地处理用户输入,我们引入了委托的概念,在此框架中使用委托的好处在于,它允许自定义呈现和编辑数据项的方式。

modelview001

通常,模型/视图类可以分为上述三个组:模型,视图和委托。这些组件中的每一个都是由抽象类定义的,这些抽象类提供了公共接口, 在某些情况下还提供了功能的默认实现。抽象类旨在被子类化,以提供其他组件期望的全部功能;这也允许编写专门的组件。

模型,视图和委托使用信号和插槽相互通信:

  • 来自模型的信号通知视图有关数据源保存的数据的更改。

  • 来自视图的信号提供有关用户与正在显示的项目的交互的信息。

  • 在编辑期间,将使用来自委托的信号来告知模型并查看有关编辑器状态的信息。

10.1. Model

模型提供了视图和委托用来访问数据的标准接口。在Qt中,标准接口由QAbstractItemModel类定义。 无论数据项如何存储在任何基础数据结构中,QAbstractItemModel的所有子类都将数据表示为包含项表的层次结构。 视图使用此约定来访问模型中的数据项,但是它们向用户呈现此信息的方式不受限制。

modelview002

QAbstractItemModel接口足够灵活,可以处理以表,列表和树的形式表示数据的视图。 但是,当为列表和类似表的数据结构实现新模型时,QAbstractListModel和QAbstractTableModel类是更好的起点, 因为它们提供了常用功能的适当默认实现。这些类中的每一个都可以被子类化以提供支持特殊类型的列表和表的模型。

为了确保数据的表示方式与访问方式分开,引入了 模型索引 的概念。

视图和委托使用这些索引来获取要显示的数据,通过模型获得的每条信息都由模型索引表示。

10.2. View

在模型/视图体系结构中,视图从模型中获取数据项并将其呈现给用户。 数据的表示方式不必类似于模型提供的数据的表示,而且可能与用于存储数据项的底层数据结构完全不同。

内容和表示的分离是通过使用qabstractemmodel提供的标准模型接口、 qabstractemview提供的标准视图接口以及以一般方式表示数据项的模型索引来实现的。 视图通常管理从模型获得的数据的总体布局,它们可以自己呈现单个数据项,或者使用代理来处理呈现和编辑功能。

除了显示数据外,视图还处理项目之间的导航以及项目选择的某些方面。 视图还实现了基本的用户界面功能,例如上下文菜单和拖放。视图可以为项目提供默认的编辑工具,也可以与委托一起提供自定义编辑器。

视图可以在没有模型的情况下构造,但必须先提供模型,然后才能显示有用的信息。 视图通过使用可为每个视图单独维护或在多个视图之间共享的选择来跟踪用户选择的项目。

Qt提供了三个现成的视图类,它们以大多数用户熟悉的方式显示模型中的数据。

  • QListView 可以将模型中的项目显示为简单列表,或者以经典图标视图的形式显示。

  • QTreeView 将模型中的项显示为列表的层次结构,允许以紧凑的方式表示深度嵌套的结构。

  • QTableView 以表格的形式显示模型中的项,很像电子表格应用程序的布局。

上面显示的标准视图的默认行为对于大多数应用程序来说应该足够了。它们提供基本的编辑工具,并且可以定制以适应更专业的用户界面的需要。

10.3. Delegates

与“模型-视图-控制器”模式不同,模型/视图设计不包括用于管理与用户交互的完全独立的组件。 通常,视图负责向用户呈现模型数据,并负责处理用户输入。为了使获取此输入的方式具有一定的灵活性,便将用户交互交给委托执行。 这些具备委托功能的组件提供输入功能,还负责在某些视图中渲染单个项目。

在QAbstractItemDelegate类中定义了用于控制委托的标准接口。

委托通过实现paint()和sizeHint()函数自己呈现其内容。 但是,基于简单窗口小部件的委托可以继承QStyledItemDelegate而不是QAbstractItemDelegate的子类,并利用这些函数的默认实现。 委托的编辑器可以通过使用小部件来管理编辑过程,也可以通过直接处理事件来实现。

10.4. Model/View的子类化

10.4.1. 便利类

上面的概念比较生涩,Qt已经就Model/View为我们提供了三个现成的类:

  • QListWidget 继承自 QListView ,提供项目列表,

  • QTreeWidget 继承自 QTreeView ,显示多级树结构,

  • QTableWidget 继承自 QTableView ,提供单元项目表。

这个三个窗口类,我们在Qt控件章节有过介绍,https://doc.embedfire.com/linux/qt/embed/zh/latest/ebf_qt/start/qt_widget.html#qlistwidget

不妨看看这个三个类的使用来回来看本章节。

在我们例程中实现了一个QtPressMoveListView类,该类继承自QListView

10.4.2. QtPressMoveListView

类的实现如下:

embed_qt_develop_tutorial_code/QtUi/src/qtpressmovelistview.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef QTPRESSMOVELISTVIEW_H
#define QTPRESSMOVELISTVIEW_H

#include <QWidget>
#include <QListView>
#include <QScrollBar>

#ifdef QtUi
#include <QtUi>
class QTUISHARED_EXPORT QtPressMoveListView : public QListView {
#else
class QtPressMoveListView : public QListView {
#endif
    Q_OBJECT
public:
    explicit QtPressMoveListView(QWidget *parent = 0);
    ~QtPressMoveListView();

    void SetIconMode();

private:
    bool m_bPressed;
    QPoint m_startPos;
    QScrollBar *m_scrollbar;
protected:
    void mousePressEvent(QMouseEvent *e);
    void mouseReleaseEvent(QMouseEvent *e);
    void mouseMoveEvent(QMouseEvent *e);
};

#endif // QTPRESSMOVELISTVIEW_H

在类中通过设置View模式,Icon大小等来使ListView达到我们要的显示效果。

embed_qt_develop_tutorial_code/QtUi/src/qtpressmovelistview.cpp
1
2
3
4
5
6
7
8
void QtPressMoveListView::SetIconMode()
{
    this->setDragEnabled(false);
    this->setViewMode(QListView::IconMode);
    this->setIconSize(QSize(60, 60));
    this->setGridSize(QSize(100, 100));
    this->setResizeMode(QListView::Adjust);
}

在 filesystemwindow.cpp 中我们使用QFileSystemModel去获取文件夹下的item, 然后调用QtPressMoveListView的setModel使文件夹下的文件数据最终显示在到ListView上面。

10.4.3. QFileSystemModel

QFileSystemModel类为本地文件系统提供了一个数据模型。

此类可以使用QAbstractItemModel提供的标准接口来访问QFileSystemModel,它还提供了一些特定于目录模型的便捷功能。 比如通过 fileInfo(), isDir(), fileName() 和 filePath() 等函数提供有关与模型中的项目的基本文件和目录信息,也可以使用mkdir(),rmdir()创建和删除目录。 在最简单的情况下,它可以与适当的显示小部件一起使用,作为浏览器或过滤器的一部分。

QFileSystemModel通过设置树视图的根索引来显示特定目录的内容 在调用 setRootPath() 之前,QFileSystemModel不会获取任何文件或目录。这样可以防止在文件系统上进行任何不必要的查询。 与QDirModel不同,QFileSystemModel使用一个单独的线程来填充自身,因此不会在查询文件系统时导致主线程挂起。 对rowCount()的调用将返回0,直到模型填充目录为止。

QFileSystemModel将保留文件信息的缓存,使用QFileSystemWatcher可以自动使缓存保持最新状态

10.5. 例程说明

野火提供的Qt Demo已经开源,仓库地址在:

文档所涉及的示例代码也提供下载,仓库地址在:

本章例程在 embed_qt_develop_tutorial_code/FileSystem

本例程为野火demo文件系统示例程序。

10.5.1. 编程思路

  • 初始化UI,设置m_listView(QtPressMoveListView实例)

  • 初始化InitModel,获取QFileSystemModel,并将设置到view中

  • 关联信号和槽,QModelIndex改变时进行不同的操作。

10.5.2. 代码讲解

这里讲解一下QtAddressBar,就是例程中显示文件路径的控件。

QtAddressBar继承自QtWidgetTitleBar,QtWidgetTitleBar在自定义控件有过详细的分析。 QtAddressBar中新增加了两个按钮和一个地址栏。

embed_qt_develop_tutorial_code/QtUi/src/qtaddressbar.cpp
1
2
3
4
5
6
7
8
void QtAddressBar::InitWidget()
{
    m_btns.insert(1, new QtPixmapButton(1, QRect(10, 10, 40, 40), QPixmap(":/images/file/ic_prev.png"), QPixmap(":/images/file/ic_prev_pre.png")));
    m_btns.insert(2, new QtPixmapButton(2, QRect(60, 10, 40, 40), QPixmap(":/images/file/ic_next.png"), QPixmap(":/images/file/ic_next_pre.png")));

    m_addrLineEdit = new AddressLineEdit(this);
    connect(m_addrLineEdit, SIGNAL(signalAddress(QString)), this, SIGNAL(signalAddress(QString)));
}

点击按钮会去调用地址栏的SltBackAddress()和SltNextAddress();

embed_qt_develop_tutorial_code/QtUi/src/qtaddressbar.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void QtAddressBar::SltBtnClicked(int index)
{
    if (0 == index) {
        emit signalBackHome();
    } else if (1 == index) {
        m_addrLineEdit->SltBackAddress();
    } else if (2 == index) {
        m_addrLineEdit->SltNextAddress();
    }
}

AddressLineEdit实现如下

embed_qt_develop_tutorial_code/QtUi/src/qtaddressbar.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AddressLineEdit : public QtWidgetBase {
    Q_OBJECT
public:
    explicit AddressLineEdit(QWidget *parent = 0);
    ~AddressLineEdit();

    void setAddress(const QString &addr);
    QString getAddress();

    void appendDir(const QString &dir);

signals:
    void signalAddress(const QString &strAddr);

public slots:
    void SltBackAddress();
    void SltNextAddress();

private:
    QMap<int,AddressItem*>  m_addressItems;
    QFont   m_font;
    int     m_nSplitWidth;
    int     m_nIndex;
    QString m_strAddrPath;
    int     m_nMargin;

private:
    void ClearItem(int startId = 0);
    void CurrentPathChanged(int index);

protected:
    void paintEvent(QPaintEvent *);
    void mousePressEvent(QMouseEvent *e);
};

设置AddressLineEdit的时候,会传入一个路径,经过处理之后有序保存在m_addressItems(QMap<int,AddressItem*> )中。 绘制控件的时候,就遍历m_addressItems,依次绘制将AddressItem绘制到AddressLineEdit中。

AddressItem 包含了一个QRect(文字绘制的地方),当我们鼠标点击QRect的时候,就会触发CurrentPathChanged的, 通过AddressItem的id来更新AddressLineEdit显示。

SltBackAddress()和SltNextAddress()id来刷新显示的,CurrentPathChanged()发生的时候, 都会发送signalAddress(strPath)信号,将当前路径传出去,

FileSystemWindow接收到这个路径的时候再更新UI。

10.5.3. 编译构建

build001
  • Ubuntu 选择 Desktop Qt 5.11.3 GCC 64bit 套件,编译运行

  • LubanCat 选择 ebf_lubancat,只编译

提示

当两种构建套件进行切换时,请重新构建项目或清除之前项目。针对我们的工程还需要手动重新构建QtUI和Skin。

build002

10.5.4. 运行结果

10.5.4.1. PC实验

直接点击编译并运行程序,结果如下:

result001

10.5.4.2. LubanCat实验

通过SCP或者NFS将编译好的程序拷贝到LubanCat上

NFS环境搭建 参考这里

SCP命令如下:

# scp传输文件
# scp 文件名 服务器上的某个用户@服务器ip:/文件保存路径
scp filename server_user_name@192.168.0.205:server_file_path
# 从服务器拉取文件
# scp 服务器上的某个用户@服务器ip:/服务器文件存放路径 拉取文件保存路径
scp server_user_name@192.168.0.229:server_file_path local_path

编译好的程序在 embed_qt_develop_tutorial_code/app_bin/FileSystem 目录中,通过scp命令将编译好的程序拉到LubanCat。

scp root@192.168.0.174:/home/embed_qt_develop_tutorial_code/app_bin/FileSystem /usr/local/qt-app/

在LubanCat运行程序,使用run_myapp.sh配置好环境,并执行 FileSystem 。

sudo /usr/local/qt-app/run_myapp.sh /usr/local/qt-app/FileSystem
result002