11. 模式和视图

模式和视图(Model/View),Qt中叫模型/视图体系结构,是进行数据存储和界面展示的一种编程结构, 在这种结构中模型存储数据,界面上的视图显示模型中的数据,在视图界面修改的数据会被自动保存到模型中。

了解Qt的模型和视图,我们先了解下MVC(模型-视图-控制器)模式,它是一种源自Smalltalk的设计模式,《设计模式》一书中有这样一句:

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

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

modelview001

在Qt中,模型/视图类包含这么几个部分:数据(data),模型(Model),视图(View)和代理(Delegates)。 - 数据(data)就是原始数据,如数据库的数据,内存中的字符串等; - 模型(Model)是与数据通信,为视图组件提供数据接口,从源数据那获取数据,用于视图的显示和编辑; - 视图(View),视图组件,从模型获取数据然后显示在屏幕上; - 代理(Delegates),是视图和模型之间交互操作的编辑器,负责从模型中获取数据,将其显示到编辑器,修改数据后又将编辑器里的数据保持到模型中。

模型,视图和代理之间使用信号与槽相互通信: 来自模型的信号通知视图有关数据源保存的数据的更改; 来自视图的信号提供有关用户与正在显示的项目的交互的信息; 在编辑期间,将使用来自委托的信号来告知模型并查看有关编辑器状态的信息。

Model/View编程参考下 Qt官方文档

11.1. 模型(Model)

模型提供了视图和代理用来访问数据的标准接口。 在Qt中,所有的模型都是基于 QAbstractItemModel 抽象类定义,QAbstractItemModel的父类是QObject,支持Qt元对象系统。 相关类的继承层次如下(部分):

modelview002

抽象类QAbstractItemModel,不能直接实例化对象,一些模型类的描述如下:

描述

QSortFilterProxyModel

与其他数据模型结合,提供排序和过滤功能的数据模型类

QStringListModel

用于处理字符串列表数据的数据模型类

QFileSystemModel

文件系统的数据模型类

QSqlTableModel

用于数据库的一个数据表的数据模型类

QStandardltemModel

标准的基于项数据的数据模型类

无论数据项如何存储在任何基础数据结构中,QAbstractItemModel的所有子类都将数据表示为表格的层次结构:

modelview002

如上图所示,层次结构有三种,分别是列表、表格和树状。 可以看出,不管数据模型的表现形式是怎么样的,数据模型中存储数据的基本单元都是项(item),每个项有一个行号(row)、一个列号(column),还有一个父项。 在列表和表格模式下,所有的项都有一个相同的顶层项(root item);在树状结构中,行号、列号、父项稍微复杂一点,但是由这3个参数完全可以定义一个项的位置,从而存取项的数据。

11.1.1. 模型索引

为了确保数据的表示方式与访问方式分开,引入了 模型索引(model index) 的概念,通过数据模型存取的每个数据都有一个模型索引, 视图和代理使用这些索引来获取要显示的数据。例如:

1
2
3
4
5
6
7
// 使用index()获得一个模型索引,对于列表和表格模式的数据模型,顶层节点总是用QModelIndex()表示
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

// 对于树状结构的数据模型,获取模型索引向压迫指定父节点,indexB是indexC的父节点:
QModelIndex indexC = model->index(2, 1, indexB);

QModelIndex 表示模型索引的类,模型索引提供数据存取的一个临时指针,用于通过数据模型提取或修改数据。

11.1.2. 项角色

存储数据的基本单元项(Items),一个项可以有不同 角色(Role) 的数据,用于不同的场合。 模型可以针对不同的组件(或者组件的不同部分,比如按钮的提示以及显示的文本等)提供不同的数据。例如下图中(显示不同角色数据的表现形式):

modelview002

其中设置Qt::DisplayRole角色是在视图组件中显示的字符串,设置Qt::ToolTipRole是鼠标提示消息,Qt::DecorationRole是设置图标装饰的数据。 我们通过下面方式获取或者设置数据角色:

1
2
3
4
5
// 设置角色的数据,简单示例
model->setData(index, QVariant(tr("yes")), Qt::EditRole);

// 获取角色的数据,简单示例
QVariant value = model->data(index, role);

role参数是一个 Qt::ItemDataRole 枚举类型,下面表格列出常见的值:

常量

描述

Qt::DisplayRole

0

直接可见的提示信息(QString)

Qt::DecorationRole

1

图标方式显示的数据(QColor, QIcon或者QPixmap)

Qt::EditRole

2

可编辑的数据信息(QString)

Qt::ToolTipRole

3

悬浮显示的提示信息(QString)

Qt::StatusTipRole

4

显示的状态信息(QString)

11.2. View

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

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

  • QListView 可以将模型中的项目显示为简单列表,显示单列的列表数据。

  • QTreeView 用于显示树状的结构,适合树状结构数据的显示。

  • QTableView 以表格的形式显示模型中的项,显示二维表格的数据。

  • QColumnView 用于多个QListView的树状结构数据,树状结构的一层用一个QListView显示,使用场景较少。

相关类的继承层次如下:

modelview002

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

上面继承关系的QTableWidget、QTreeWidget和QListWidget,它们分别继承自QTableView、QTreeView、QListView, 分别提供表格列表、多级树结构和项目列表功能。这三个都是Qt提供的现成的类, 可以直接使用,称为 便利类(Convenience Classes),这三个类我们前面基础控件章节有简单讲解。

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

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

11.3. Delegates

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

QAbstractItemDelegate 类是代理(Delegates)基类,是抽象类,不能直接创建对象。 该类有两个子类 QItemDelegateQStyledItemDelegate

代理就是在视图组件上为编辑数据提供临时的编辑器,负责从模型中获取数据,将其显示到编辑器,修改数据后又将编辑器里的数据保持到模型中。 例如:QTableWidget上单击一个单元格,代理会临时提供一个临时的编辑器,默认是QLineEdit, 在这个单元格里修改项内容,回车或者焦点移动到其他单元格时完成编辑,编辑的内容会自动保存到数据模型。

11.3.1. 自定义代理

对于一些特殊需要,比如从列表中选择数据,只输入整数数据等,我们可以将QComboBox,QSpinBox作为代理类,也就是可以 自定义代理

自定义代理类需要从QStyledItemDelegate类继承,创建自定义代理示例后,就可以设置为视图的代理,视图组件某行或者某列的代理,取代默认代理。 一个简单自定义代理的实现,一般需要重新实现这四个虚函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
QWidget * QStyledItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option,
                      const QModelIndex &index) ;

void QStyledItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index);

void QStyledItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
                  const QModelIndex &index);

void QStyledItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option,
                          const QModelIndex &index);

createEditor() 函数用于创建编辑器的界面组件,比如我们可以创建QComboBox或者QSpinBox; setEditorData() 函数从模型数据中获取某项,然后将其设置为代理编辑器上显示的数据; setModelData() 函数从代理编辑器中获取数据,保存到模型; updateEditorGeometry() 函数来更新编辑器的位置和大小。

自定义代理的简单示例参考下配套例程。

11.4. 配套例程

例程将简单测试列表和表格模型,使用ListView和TableVieW组件显示,另外还添加自定义代理,简单流程:

  • 创建一个工程项目,基于QMainWindow;

  • 初始化UI,在界面添加ListView和TableVieW;

  • 添加自定义代理类ComboboxDelegate;

  • 初始化模型,添加数据;

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

11.4.1. 程序说明

创建模型并关联模型和视图组件:

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void MainWindow::init_StringListModel()
{
    // 设置数据
    list<<"第一"<<"第二 "<<"第三"<<"第四"<<"第五";

    // 设置模型数据
    listModel.setStringList(list);

    // 视图,设置使用的模型
    ui->listView->setModel(&listModel);

    connect(ui->btn_add,&QPushButton::clicked,this,&MainWindow::StringListModel_add);
    connect(ui->btn_remove,&QPushButton::clicked,this,&MainWindow::StringListModel_remove);
    connect(ui->btn_initList,&QPushButton::clicked,this,&MainWindow::StringListModel_initList);
    connect(ui->btn_instert,&QPushButton::clicked,this,&MainWindow::StringListModel_instert);
}

void MainWindow::init_StandardItemModel()
{
    // 创建一个模型
    m_model = new QStandardItemModel(3,2,this);

    // 初始化模型
    for(int r=0;r<m_model->rowCount();r++)
    {
        for(int c=0;c<m_model->columnCount();c++)
        {
            QStandardItem* item=new QStandardItem(QString("行 %0, 列 %1").arg(r).arg(c));
            m_model->setItem(r,c,item);
        }
    }
    // tableView设置数据模型
    ui->tableView->setModel(m_model);

    //在最后列,插入一列
    m_model->insertColumn(m_model->columnCount());
    // 初始化
    for(int r=0;r<m_model->rowCount();r++)
    {
        QStandardItem* item=new QStandardItem(QString("第一"));
        m_model->setItem(r,m_model->columnCount()-1,item);
    }

    combo_delegate = new ComboboxDelegate(this);
    QStringList strList;
    strList<<"第一"<<"第二"<<"第三"<<"第四";
    combo_delegate->setItems(strList);
    // 设置最后一列为自定义代理,当编辑时显示Combobox选择
    ui->tableView->setItemDelegateForColumn(m_model->columnCount()-1, combo_delegate);
}

继承自QStyledItemDelegate,重新实现虚函数,创建自定义代理类:

 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
35
36
37
38
39
40
41
QWidget *ComboboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(option);
    Q_UNUSED(index);

    // 创建一个QComboBox
    QComboBox *editor = new QComboBox(parent);
    // 初始QComboBox下拉列表
    for (int i=0;i<itemList.count();i++)
        editor->addItem(itemList.at(i));

    return editor;
}

void ComboboxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    QString str = index.model()->data(index, Qt::EditRole).toString();

    // 获具体comboBox
    QComboBox *comboBox = static_cast<QComboBox*>(editor);
    // 设置内容
    comboBox->setCurrentText(str);
}

void ComboboxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QComboBox *comboBox = static_cast<QComboBox*>(editor);

    // 获取当前数据
    QString str = comboBox->currentText();

    // 设置模型数据
    model->setData(index, str, Qt::EditRole);
}

void ComboboxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    Q_UNUSED(index);
    // 调整大小位置
    editor->setGeometry(option.rect);
}

11.4.2. 编译构建

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

  • LubanCat 选择 lubancat_rk,只编译。

提示

当两种构建套件进行切换时,请重新构建项目或清除之前项目。

11.4.3. 运行测试

11.4.4. PC实验

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

result001

板卡上测试类似。