Qt进阶开发:模型/视图原理详解

一、模型/视图架构概述

  应用程序中往往要存储大量的数据,并对它们进行处理,然后可以通过各种形式显示给用户,用户需要时还可以对数据进行编辑。Qt中的模型/视图架构就是用来实现大量数据的存储、处理及其显示的。Qt4中引入了一组新的项视图类,它们使用一个模型/视图架构来管理数据、展示给用户的方式之间的关系。这种架构引入的功能分离思想为开发者定制项目的显示提供了高度的灵活性,而且还提供了一个标准的模型接口来允许大范围的数据源使用已经存在的项目视图。
  MVC(Model-View-Controller)是一种起源于Smalltalk的设计模式,经常用于创建用户界面。MVC包含了3个组件:模型(Model)是应用对象,用来表示数据:视图(View)是模型的用户界面,用来显示数据:控制(Controller),定义了用户界面对用户输入的反应方式。在MVC之前,用户界面设计都是将这3种组件集成在一起。而MVC将它们分离开,从而提高广灵活性和重用性。如果将视图和控制两种组件结合起来,就形成了模型/视图架构。这同样将数据的存储和数据向用户的展示进行了分离,但提供了更为简单的框架。数据和界面进行分离,使得相同的数据在多个不同的视图中进行显示成为可能,而且还可以创建新的视图,而不需要改变底层的数据框架。为了对用户输入进行灵活处理,还引入了委托(Delegate,也被称为代理的概念,使用它可以定制数据的渲染和编辑方式。

  模型/视图的整体架构如下图所示。其中,模型与数据源进行通信,为架构中的其他组件提供了接口。视图从模型中获得模型索引(ModelIndex),模型索引用来表示数据项。在标准的视图中,委托渲染数据项,编辑项目时,委托使用模型索引直接与模型进行通信。
在这里插入图片描述

二、模型/视图架构的组成部分

  模型/视图架构中的众多类可以分为3组:模型、视图和委托。其中,每一个组件都使用了一个抽象基类来定义,提供了一些通用接口和一些功能的默认实现。模型、视图、委托之间使用信号和槽来实现通信:

  • 当数据源的数据发生改变时,模型发出信号告知视图。
  • 当用户与显示的项目交互时,视图发出信号来提供交互信息;
  • 当编辑项目时,委托发出信号,告知模型和视图编辑器的状态。

2.1 模型

  所有的模型都基于QAbstractItemModel类,这个类定义了一个接口,可以供视图和委托来访问数据。数据本身并不一定要存储在模型中,也可以存储在一个数据结构、一个独立的类、文件、数据库或者应用程序的其他一些组件中。QAbstractItemModel为数据提供了一个十分灵活的接口来处理各种视图,这些视图可以将数据表现为表格(table)、列表(list)和树(tree)等形式。然而,要实现一个新的模型时,如果它基于列表或者表格的数据结构,那么可以使用QAbstractListModel 和QAbstractTableModel类,因为它们为一些常见的功能提供了默认的实现。这些类都可以被子类化来提供模型,从而支持特殊类型的列表和表格。

Qt提供了一些现成的模型来处理数据项:

  • QStringListModel用来存储一个简单的QString项目列表。
  • QStandardItemModel管理复杂的树型结构数据项,每一个数据项可以包含任意的数据。
  • QSqlQueryModelQSqlTableModel和QSqlRelationalTableModel用来访问数据库。
  • QFileSystemModel提供了本地文件系统中文件和目录的信息。

如果Qt提供的这些标准模型无法满足需要,还可以子类化QAbstractItemModel、QAbstractListModel或者QAbstractTableModel来创建自定义的模型。

2.2 视图

  Qt提供了几种不同类型的视图:QListView将数据项显示为一个列表,QTableView将模型中的数据显示在一个表格中,QTreeView将模型的数据项显示在具有层次的列表中。这些类都基于QAbstractItemView抽象基类,这些类可以直接使用,也可以被子类化来提供定制的视图。

2.3 委托

  在模型/视图框架中,QAbstractItemDelegate是委托的抽象基类。从Qt4.4开始,默认的委托实现由QStyledItemDelegate类提供,这也被用作Qt标准视图的默认委托。然而,QStyledItemDelegate和QItemDelegate是相互独立的,只能选择其一来为视图中的项目绘制和提供编辑器。它们的主要不同就是,QStyledItemDelegate使用当前的样式来绘制项目,因此,当要实现自定义的委托或者要和Qt样式表一起应用时建议使用OStyledItmDelegate作为基类。

简单示例:

#include <QApplication>
#include <QFileSystemModel>
#include <QTreeView>
#include <QListView>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 创建文件系统模型
    QFileSystemModel model;
    // 指定要监视的目录
    model.setRootPath(QDir::currentPath());
    // 创建树型视图
    QTreeView tree;
    // 为视图指定模型
    tree.setModel(&model);
    // 指定根索引
    tree.setRootIndex(model.index(QDir::currentPath()));
    // 创建列表视图
    QListView list;
    list.setModel(&model);
    list.setRootIndex(model.index(QDir::currentPath()));

    tree.setWindowTitle("TreeView---测试");
    tree.resize(800, 600);
    tree.show();

    list.setWindowTitle("ListView---测试");
    list.resize(800, 600);
    list.show();

    return a.exec();
}

在这里插入图片描述
创建新的模型:

#include <QObject>
#include <QAbstractListModel>
#include <QStringList>

class StringListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    StringListModel(const QStringList stringList, QObject *parent = 0)
        : QAbstractListModel(parent)
        , m_stringList(stringList) {
    }

	/*
		因为这个模型是非层次结构的,可以忽略掉模型索引对应的交项目,所以这只需要简单地返回字符串列表中的字符串个数即可。
		默认的,继承自QAbstractListModel的模型只包含一列,所以这里不需要实现columnCountO函数。
	*/
    int rowCount(const QModelIndex &parent = QModelIndex()) const {
        return m_stringList.count();
    }

	/*
		对于视图中的项目我们想要显示为字符串列表中的字符串,这个函数就是用来返回对应索引参数的数据项的。
		当提供的索引是有效的,行号在字符串列表的大小范围之内,而且需要的角色是支持的角色之一时,返回一个有效的QVariant。
	*/
    QVariant data(const QModelIndex &index, int role) const {
        if (!index.isValid()) {
            return  QVariant();
        }

        if (index.row() == m_stringList.size()) {
            return  QVariant();
        }

        if (role == Qt::DisplayRole) {
            return  m_stringList.at(index.row());
        } else {
            return QVariant();
        }
    }

	/*
		像QTreeView和QTableView等一些视图,在显示项目数据的同时还会显示标头。这里实现了在标头中显示行号和列号。
		并不是所有的视图都会显示标头,一些视图会隐藏它们。不过,还是建议通过headerData()函数来提供数据的相关信息。
	*/
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const {
        if (role != Qt::DisplayRole) {
            return QVariant();
        }

        if (orientation == Qt::Horizontal) {
            return QString("Column %1").arg(section);
        } else {
            return QString("Row %1").arg(section);
        }
    }

private:
    QStringList m_stringList;
};

  这里实现的模型是一个简单的、非层次结构的、只读的数据模型,它基于标准的QStringListModel类。该模型使用了一个QStringList作为内部的数据源,这是因为QAbstractItemModel本身是不存储任何数据的,它仅仅提供了一些接口来供视图访问数据。在模型类的定义中,除了构造函数以外,只需要实现两个函数:rowCount()和data(),前者返回模型的行数,后者返回指定模型索引的数据项。这里还实现了headerData()函数,它可以在树和表格视图的标头显示一些内容。注意,因为现在的模型是非层次结构的,所以不需要考虑父子关系。但是,如果模型是层次结构的,那么还需要实现index()和parent()函数。

现在一个只读的数据模型类就创建完成了,测试该模型是否可以正常使用:

#include <QApplication>
#include <QListView>
#include <QTableView>
#include "stringlistmodel.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QStringList list;
    list << "a" << "b" << "c";

    StringListModel model(list);
    QListView listView;
    listView.setModel(&model);
    listView.resize(800, 600);
    listView.show();

    QTableView tableView;
    tableView.setModel(&model);
    tableView.resize(800, 600);
    tableView.show();

    return a.exec();
}

在这里插入图片描述
添加编辑功能:
为了使模型可以编辑,需要更改data()函数,然后实现另外两个函数:flags()和 setData()。

 Qt::ItemFlags flags(const QModelIndex &index) const {
        if (!index.isValid()) {
            return  Qt::ItemIsEnabled;
        }
        
        return  QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
    }

  委托创建编辑器之前会检测项目是否是可编辑的,模型必须让委托知道它的项目是可编辑的,这里为模型中的每一个项目返回一个正确的标识来达到这个目的。注意,并不需要知道委托是怎样执行真正的编辑操作的,而只需要为委托向模型中设置数据提供一条途径,这个是通过setData()函数实现的:

bool setData(const QModelIndex &index, const QVariant &value, int role) {
        if (index.isValid() && role == Qt::EditRole) {
            m_stringList.replace(index.row(), value.toString());
            emit dataChanged(index, index);
            
            return  true;
        }
        
        return  false;
    }

  在这个模型中,字符串列表里对应指定的模型索引的项目被参数中提供的value值替换掉了。不过,在修改字符串列表以前,必须保证索引是有效的、项目是正确的类型、而且角色是被支持的。当数据被设置后,模型必须让视图知道有数据已经改变了。这是通过发射dataChanged()信号实现的。
  最后,还需要更改前面添加的data函数中的判断条件,为其添加Qt::EditRole测试。

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

        if (index.row() == m_stringList.size()) {
            return  QVariant();
        }

        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            return  m_stringList.at(index.row());
        } else {
            return QVariant();
        }
    }

现在运行程序后,可以发现已经能够编辑数据了。
请添加图片描述
插入和删除行:
  要想实现在模型中插入和删除行,需要重新实现insertRows()和removeRow()两个函数。

/*
	因为模型中的行对应着列表中的字符串,这个函数就是在指定位置的前面添加了指定数量的空字符串。
	父索引是用来决定在模型的什么地方添加行的,因为这里只有单一的顶层字符串列表,所以只需要向列表中添加空的字符串。
	模型首先要调用beginInsertRows()函数来告知其他组件指定的行将要发生改变,这个函数指定了将要插入的第一个、最后一个新行的行号,以及它们父项的模型索引。
	当改变完字符串以后,调用了endInsertRows()函数来完成操作,而且告知其他组件该模型的大小发生了变化。
*/
 bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) {
        beginInsertRows(QModelIndex(), position, position + rows - 1);
        for (int row = 0; row < rows; ++row) {
            m_stringList.insert(position, "");
        }
        endInsertRows();

        return  true;
    }

    bool removeRows(int position, int rows, const QModelIndex &parent = QModelIndex()) {
        beginRemoveRows(QModelIndex(), position, position + rows - 1);
        for (int row = 0; row < rows; ++row) {
            m_stringList.removeAt(position);
        }
        endRemoveRows();

        return  true;
    }

在主函数代码中添加这个两个函数:

 model.insertRows(3, 2);
 model.removeRows(0,1);

这样便在模型最后添加两个空数据项,并删除了模型的第一个数据项。

三、模型类的介绍

  在模型/视图架构中,模型提供了一个标准的接口供视图和委托来访问数据。在Qt中这个标准的接口使用QAbstractItemModel类来定义。无论数据项是怎样存储在何种底层数据结构中,QAbstracttemModel的子类都会以层次结构来表示数据:这个结构中包含了数据项表。视图按照这种约定来访问模型中的数据项:但是这不会影响数据的显示,视图可以使用任何形式将数据显示出来。当模型中的数据发生变化时,模型会通过信号和槽机制告知与其相关联的视图。

常见的3种模型分别是列表模型、表格模型和树模型,它们的示意图如下图所示。
在这里插入图片描述

图 1-1 常见的3种模型的示意图

3.1 模型索引

  为了确保数据的表示与数据的获取相分离,Qt引入了模型索引的概念。每一块可以通过模型获取的数据都使用一个模型索引来表示,视图和委托都使用这些索引来请求数据项并显示。这样只有模型需要知道怎样获取数据,被模型管理的数据类型可以广泛地被定义。模型索引包含一个指针,指向创建它们的模型,便用多个模型时可以免混淆。
  模型索引由QModellndex类提供,它是对一块数据的临时引用,可以用来检索或者修改模型中的数据。因为模型可能随时对内部的结构进行重新组织,这样模型索引可能失效。所以不需要也不应该存储模型索引。如果需要对一块数据进行长时间的引用,则必须使用QPersistentModelIndex创建模型索引。如果要获得一个数据项的模型索引。则必须指定模型的3个属性:行号、列号和父项的模型索引,例如:

QModelIndex index = model->index(row,column,parent);

其中,row、column和parent分别代表了这3个属性。

3.2 行和列

  在最基本的形式中,一个模型可以把它看作一个简单的表格来访问,这时每个数据项可以使用行号和列号来定位。但这并不意味着底层的数据块是存储在数组结构中的,使用行号和列号只是一种约定,以确保各组件间可以相互通信。
  行号和列号都是从0开始的,在图 1- 1中可以看到,列表模型和表格模型的所有数据项都是以根项(Rootitem)为父项的,这些数据项都可以被称为顶层数据项(Top levelitem);在获取这些数据项的索引时,父项的模型索引l可以用QModelInd()表示。例如,图1-1中的TableModel中的A、B、C这3项的模型索引可以用如下代码获取:

QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex()); 
QModelIndex indexC = model->index(2, 1, QModelIndex());

3.3 父项

  前面讲述的类似于表格的接口对于在使用表格或者列表时是非常理想的,但是,像树视图一样的结构需要模型提供一个更加灵活的接口,因为每一个数据项都可能成为其他数据项表格的父项,一个树视图中的顶层数据项也可能包含其他的数据项列表。当为模型项请求一个索引时,就必须提供该数据项父项的一些信息。前面讲到,顶层数据项可以使用QModelIndex()作为父项索引,但是在树模型中,如果一个数据项不是顶层数据项,那么就要指定它的父项索引。例如,图1-1中的TreeModel中的A、B、 C这三项的模型索引可以使用如下代码获得:

QModelIndexindexA = model->index(0, 0, QModelIndex()); 
QModelIndexindexC = model->index(2, 0, QModelIndex()) 
QModelIndexindexB = model->index(1, 0, indexA);

4.项角色

  模型中的数据项可以作为各种角色在其他组件中使用,允许为不同的情况提供不同类型的数据。例如,Qt::DisplayRole用于访问一个字符串,所以可以作为文本显示在视图中。通常情况下,数据项包含了一些不同角色的数据,这些标准的角色由枚举类型Qt::ItemDataRole来定义,常用的角色如 表1-2所列。要查看全部的角色类型,可以在帮助中通过Qt::ItemDataRole关键字查看。通过为每个角色提供适当的项目数据,模型可以为视图和委托提供提示,告知数据应该怎样展示给用户。角色指出了从模型中引用哪种类型的数据,视图可以使用不同的方式来显示不同的角色,如图 1-3 所示。不同类型的视图也可以自由地解析或者忽略这些角色信息。可以通过向模型指定相关数据项对应的模型索引以及特定的角色来获取需要的类型的数据,例如:

QVariant value = model->data(index,role):

在这里插入图片描述

图 1-3 项角色示意图

在这里插入图片描述

表 1-2 常用的角色类型

#include <QApplication>
#include <QStandardItemModel>
#include <QTreeView>
#include <QDebug>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 创建标准项模型
    QStandardItemModel model;
    // 获取模型的根项(Root Item),根项是不可见的
    QStandardItem *parentItem = model.invisibleRootItem();
    // 创建标准项item0,并设置显示文本,图标和工具提示
    QStandardItem *item0 = new QStandardItem;
    item0->setText("A");
    QPixmap pixmap0("50, 50");
    pixmap0.fill("red");
    item0->setIcon(QIcon(pixmap0));
    item0->setToolTip("indexA");
    // 将创建的标准项作为根项的子项
    parentItem->appendRow(item0);
    // 将创建的标准项作为新的父项
    parentItem = item0;
    // 创建新的标准项,它将作为item0的子项
    QStandardItem *item1= new QStandardItem;
    item1->setText("B");
    QPixmap pixmap1(50, 50);
    pixmap1.fill("blue");
    item1->setIcon(QIcon(pixmap1));
    item1->setToolTip("indexB");
    parentItem->appendRow(item1);
    // 创建新的标准项,这里使用了另一种方法来设置文本、图标和工具提示
    QStandardItem *item2 = new QStandardItem;
    QPixmap pixmap2(50, 50);
    pixmap2.fill("green");
    item2->setData("C", Qt::EditRole);
    item2->setData("indexC", Qt::ToolTipRole);
    item2->setData(QIcon(pixmap2), Qt::DecorationRole);
    parentItem->appendRow(item2);
    // 在树视图中显示模型
    QTreeView view;
    view.setModel(&model);
    view.show();

    QModelIndex indexA = model.index(0, 0, QModelIndex());
    qDebug() << "indexA row count:" << model.rowCount(indexA);

    QModelIndex indexB = model.index(0, 0, indexA);
    qDebug() << "indexB text:" << model.data(indexB, Qt::EditRole).toString();
    qDebug() << "indexB toolTip" << model.data(indexB, Qt::ToolTipRole).toString();

    return a.exec();
}

在这里插入图片描述
这里使用了标准项模型QStandardItemModel,该类提供了一个通用的模型来存储自定义的数据。QStandardItemModel中的项由QStandardItem类提供,该类为项目的创建提供了很多便捷函数,比如设置图标的setIcon()函数等。当然,也可以不使用这些函数,而是使用setData函数,并且指定项角色,如程序中创建item2就是使用的这种方法。通过代码可以看到,获取模型的大小可以使用rowCount()和columnCount()等函数;可以使用模型索引来访问模型中的项目,但是需要指定其行号、列号和父模型索引;当要访问顶层项目时,父模型索引可以使用QModellIndex()来表示;如果项目包含不同角色的数据,那么获取数据时要指定相应的项角色。

四、视图类的介绍

4.1 基本概念

  在模型/视图架构中,视图包含了模型中的数据项,并将它们呈现给用户,而数据的表示方法可能与底层用于存储数据项的数据结构完全不同。这种内容与表现的分离之所以能够实现,是因为使用了QAbstractItemModel提供的一个标准模型接口,还有QAbstractItemView提供的一个标准视图接口,以及使用模型索引提供了一种通用的方法来表示数据。视图通常管理从模型获取的数据的整体布局,它们可以自已渲染独立的数据项,也可以使用委托来处理渲染和编辑。
  除了呈现数据,视图还处理项目间的导航,以及项自选择的某些方面,表 2-1和表 2-2分别罗列了视图中的选择行为(QAbstractItemView::SelectionBehavior)和选择模式(QAbstractItemView::SelectionMode),后面的内容中会看到它们的应用。视图也实现了一些基本的用户接口特性,比如上下文菜单和拖放等。视图可以为项自提供默认的编辑实现,当然也可以和委托一起来提供一个自定义的编辑器。不指定模型也可以构造一个视图,但是在视图显示有用的信息以前,必须为其提供一个模型。
在这里插入图片描述

表 2-1 视图类的选择行为

在这里插入图片描述
表 2-2 视图类的选择模式

  对于一些视图,如QTableView和QTreeView,在显示项目的同时还可以显示标头。这是通过QHeaderView类实现的,它们使用QAbstractItemModel::headerData(函数从模型中获取数据,然后一般使用一个标签来显示标头信息。可以通过子类化 QHeaderView类来设置标签的显示。
  Qt中已经提供了QListView、QTableView和QTreeView这3个现成的视图,不过都是使用规范的格式显示数据。如果想要实现条形图或者饼状图等特殊显示方式,就要重新实现视图类了。

4.2 处理项目选择

  模型/视图架构中对项目的选择提供了非常方便的处理方法。在视图中被选择的项目的信息存储在一个QItemSelectionModel实例中,这样被选择项目的模型索引便保持在一个独立的模型中,与所有的视图都是独立的。在一个模型上设置多个视图时,就可以实现在多个视图之间共享选择。
  选择由选择范围指定,只需要记录每一个选择范围开始和结束的模型索引即可,非连续的选择可以使用多个选择范围来描述。选择可以看作是在选择模型中保存的一个模型索引集合,最近的项目选择被称为当前选择。

1.当前项目和被选择的项目
  视图中,总是有一个当前项目和一个被选择的项目,两者是两个独立的状态。在同一时间,一个项目可以既是当前项目,同时也是被选择的项目。视图负责确保总是有一个项目作为当前项目来实现键盘导航。当前项自和被选择的项目的区别如表 2-3所列。
在这里插入图片描述

表 2-3 当前项目和被选择的项目的区别

  当操作选择时,可以将QItemSelectionModel看作一个项目模型中所有项目的选择状态的一个记录。一旦设置了一个选择模型,所有的项目集合都可以被选择、取消选择或者切换选择状态,而不需要知道哪一个项自已经被选择了。所有被选择项目的索引都可以被随时进行检索,其他的组件也可以通过信号和槽机制来获取选择模型的改变信息。

2.使用选择模型
  标准视图类中提供广默认的选择模型,可以在大多数的应用中直接使用。属于一个视图的选择模型可以使用这个视图的selectionModel()函数获得,而且还可以在多个视图之间使用setSelectionModel()函数来共享该选择模型,所以一般不需要重新构建一个选择模型。下面通过例子来看一下选择模型的使用。

#include <QWidget>
#include <QStandardItemModel>
#include <QTableView>
#include <QDebug>

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) {
        QStandardItemModel *model = new QStandardItemModel(7, 4, this);
        for (int row = 0; row < 7; ++ row) {
            for (int column = 0; column < 4; ++column) {
                QStandardItem *item = new QStandardItem(QString("%1").arg(row * 4 + column));
                model->setItem(row, column, item);
            }
        }

        m_tableView = new QTableView(this);
        m_tableView->resize(800, 600);
        m_tableView->setModel(model);
        // 获取视图的项目选择模型
        QItemSelectionModel *selectionModel = m_tableView->selectionModel();
        // 定义左上角和右上角的索引,然后使用这两个索引创建选择
        QModelIndex topLeft;
        QModelIndex bottomRight;
        topLeft = model->index(0, 0, QModelIndex());
        bottomRight = model->index(5, 3, QModelIndex());
        QItemSelection selection(topLeft, bottomRight);
        // 使用指定的选择模式来选择项目
        selectionModel->select(selection, QItemSelectionModel::Select);
    }

private:
    QTableView* m_tableView;
};

在这里插入图片描述
  要获取选择模型中的模型索引可以使用selectedIndexes()函数,它会返回一个模型索号的列表,然后遍历这个列表即可。当选择模型中选择的项目改变时,会发射相关信号。

connect(selectionModel, &QItemSelectionModel::selectionChanged, this, &MainWindow::updateSelection);
connect(selectionModel, &QItemSelectionModel::currentChanged, this, &MainWindow::changeCurrent);

```cpp
 // 更新选择
 void updateSelection(const QItemSelection &selected, const QItemSelection &deselected) {
      QModelIndex index;
      QModelIndexList list = selected.indexes();
      // 为现在选择的项目充值
      foreach (index, list) {
           QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
            m_tableView->model()->setData(index, text);
        }
       list = deselected.indexes();
       // 清空上一次选择的项目的内容
       foreach (index, list) {
           m_tableView->model()->setData(index, "");
       }
 }

 // 改变当前项目
 void changeCurrent(const QModelIndex &current, const QModelIndex &previous) {
      qDebug() << tr("move(%1,%2) to (%3, %4)").arg(previous.row()).arg(previous.column())
                    .arg(current.row()).arg(current.column());
 }

输出结果:
在这里插入图片描述

五、委托类的介绍

5.1 基本概念

  与Model-View-Controller模式不同,模型/视图结构中没有包含一个完全分离的组件来处理与用户的交互。一般的,视图用来将模型中的数据展示给用户,也用来处理用户的输入,这个在前面的程序中已经看到了。为了获得更高的灵活性,交互可以由委托来执行。这些组件提供输入功能,而且也负责渲染一些视图中的个别项目。控制委托的标准接口在QAbstractItemDelegate类中定义。委托通过实现paint()和sizeHint()函数来使它们可以渲染自身的内容。然而,简单的基于部件的委托可以通过子类化QItemDelegate来实现,而不需要使用QAbstractItemDelegate,这样可以使用这些函数的默认项实现。委托的编辑器可以通过两种方式来实现,一种是使用部件来管理编辑过程,另一种是直接处理事件。
  Qt中的标准视图都使用QItemDelegate的实例来提供编辑功能,这种委托接口的默认实现为QListView、QTableView和QTreeView等标准视图的每一个项目提供了普通风格的染。标准视图中的默认委托会处理所有的标准角色,可以使用itemDelegate()函数获取一个视图中使用的委托,使用setItemDelegate()函数可以为一个视图安装一个自定义委托。

5.2 自定义委托

#include <QItemDelegate>
#include <QSpinBox>

class SpinBoxDelegate : public QItemDelegate
{
    Q_OBJECT
public:
    explicit SpinBoxDelegate(QObject *parent = nullptr) : QItemDelegate(parent) {

    }

    /*
        当视图需要一个编辑器时,它会告知委托为被修改的项目提供一个编辑器部件
        这里的createEditor函数为委托设置一个合适的部件提供了所需要的一切。
        在这个函数中,并不需要为编辑器部件保持一个指针,因为视图会负责在不再需要该编辑器时销毁它。
    */
    // 创建编辑器
    QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        QSpinBox *editor = new QSpinBox(parent);
        editor->setMinimum(0);
        editor->setMaximum(100);

        return editor;
    }

    /*
        委托必须将模型中的数据复制到编辑器中,这里已经知道了编辑器部件是一个QSpinBox,
        但是,也可能需要为模型中不同类型的数据提供不同的编辑器,所以要在访问部件的成员函数以前将它转换为合适的类型
    */
    // 为编辑器设置数据
    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        int value = index.model()->data(index, Qt::EditRole).toUInt();
        QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
        spinBox->setValue(value);
    }

    /*
        当用户完成了对QSpinBox部件中数据的编辑时,视图会通过调用setModelData函数来告知委托将编辑好的数据存储到模型中。
        这里调用了interpretText()函数来确保获得的是QSpinBox中最近更新的数值。
        标准的QItemDelegate类会在完成编辑后发射closeEditor()信号来告知视图,视图确保编辑器部件被关闭和销毁。
        而这里只是提供了简单的编辑功能,并不需要发射这个信号。
    */
    // 将数据写入到模型中
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
        QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
        spinBox->interpretText();
        int value = spinBox->value();
        model->setData(index, value, Qt::EditRole);
    }

    /*
        委托有责任来管理编辑器的几何布局,必须在创建编辑器以及视图中项目的大小或位置改变时设置它的几何布局,
        视图使用了一个QStyleOptionViewItem对象来提供了所有需要的几何布局高信息。
        这里只使用了项自的矩形作为编辑器的几何布局,而对于更复杂的编辑器部件,可能需要将这个矩形进行分割。
    */
    // 更新编辑器几何布局
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
};

调用委托的示例代码:

#include <QWidget>

#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QItemSelectionModel>

#include <QWidget>
#include <QStandardItemModel>
#include <QTableView>
#include <QDebug>

#include "spinboxdelegate.h"

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) {
        QStandardItemModel *model = new QStandardItemModel(7, 4, this);
        for (int row = 0; row < 7; ++ row) {
            for (int column = 0; column < 4; ++column) {
                QStandardItem *item = new QStandardItem(QString("%1").arg(row * 4 + column));
                model->setItem(row, column, item);
            }
        }

        SpinBoxDelegate *delegate = new SpinBoxDelegate(this);

        m_tableView = new QTableView(this);
        m_tableView->setItemDelegate(delegate);
        m_tableView->resize(800, 600);
        m_tableView->setModel(model);
        // 获取视图的项目选择模型
        QItemSelectionModel *selectionModel = m_tableView->selectionModel();
        // 定义左上角和右上角的索引,然后使用这两个索引创建选择
        QModelIndex topLeft;
        QModelIndex bottomRight;
        topLeft = model->index(0, 0, QModelIndex());
        bottomRight = model->index(5, 3, QModelIndex());
        QItemSelection selection(topLeft, bottomRight);
        // 使用指定的选择模式来选择项目
        selectionModel->select(selection, QItemSelectionModel::Select);
    }

private:
    QTableView* m_tableView;
};

请添加图片描述
  编辑完成后,委托应该为其他组件提供提示,告知它们编辑操作的结果,提供提示也有利于后续的编辑操作。这个可以通过发射colseEditor()信号时使用合适的提示来实现,它们会被构造编辑器时安装的默认QItemDelegate事件过滤器捕获。可以通过调整编辑器的行为来使得它更加友好。对于QItemDelegate提供的默认的事件过滤器,如果用户在spinbox编辑器中按下回车键,那么委托就会向模型提交数值,然后关闭编辑器。可以通过在spinbox上安装自已的事件过滤器来改变这个行为,并提供编辑提示来迎合我们的需要。比如,可以在发射colseEditor()时使EditNextItem提示来实现在视图中自动编辑下一个项目。
  另一种不需要使用事件过滤器的方式是提供自定义的编辑器部件,比如子类化 QSpinBox。这种方式可以对编辑器的行为提供更多的控制,不过它是以编写更多的代码为代价的。一般的,如果需要自定义一个标准的Qt编辑器部件的行为,则在委托中安装一个事件过滤的方式更加简便。

六、数据-窗口映射器

  数据-窗口映射器QDataWidgetMapper类在数据模型的一个区域和一个窗口部件间提供了一个映射,这样就可以实现在一个窗口部件上显示和编辑一个模型中的一行数据。

clude <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QDataWidgetMapper>
#include <QStandardItemModel>

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) {
        this->resize(800, 600);

        m_pName = new QLabel("姓名:", this);
        m_pName->setGeometry(200, 200, 40, 40);

        m_pNameText = new QLineEdit(this);
        m_pNameText->setGeometry(m_pName->pos().x() + 60, m_pName->pos().y(), 200, 40);


        m_pScore = new QLabel("分数:", this);
        m_pScore->setGeometry(m_pName->pos().x(), m_pName->pos().y() + 60, 40, 40);

        m_pScoreText = new QLineEdit(this);
        m_pScoreText->setFixedSize(200, 40);
        m_pScoreText->setGeometry(m_pScore->pos().x() + 60, m_pName->pos().y() + 60, 200, 40);


        m_pPreviousBtn = new QPushButton("上一条", this);
        m_pPreviousBtn->setGeometry(m_pName->pos().x() + 40, m_pName->pos().y() + 120, 100, 40);
        connect(m_pPreviousBtn, &QPushButton::clicked, this, &MainWindow::onPreviousButtonClicked);

        m_pNextBtn = new QPushButton("下一条", this);
        m_pNextBtn->setGeometry(m_pPreviousBtn->pos().x() + 160, m_pPreviousBtn->pos().y(), 100, 40);
        connect(m_pNextBtn, &QPushButton::clicked, this, &MainWindow::onNextButtonClicked);

        QStandardItemModel *model = new QStandardItemModel(3, 2, this);
        model->setItem(0, 0, new QStandardItem("小明"));
        model->setItem(0, 1, new QStandardItem("90"));
        model->setItem(1, 0, new QStandardItem("小红"));
        model->setItem(1, 1, new QStandardItem("88"));
        model->setItem(2, 0, new QStandardItem("小刚"));
        model->setItem(2, 1, new QStandardItem("99"));
        m_pMapper = new QDataWidgetMapper(this);
        // 设置模型
        m_pMapper->setModel(model);
        // 设置窗口部件和模型中的列的映射
        m_pMapper->addMapping(m_pNameText, 0);
        m_pMapper->addMapping(m_pScoreText, 1);
        // 显示模型中的第一行
        m_pMapper->toFirst();
    }

private slots:
    void onPreviousButtonClicked() {
        m_pMapper->toPrevious();
    }

    void onNextButtonClicked(){
        m_pMapper->toNext();
    }

private:
    QLabel *m_pName;
    QLabel *m_pScore;

    QLineEdit *m_pNameText;
    QLineEdit *m_pScoreText;

    QPushButton *m_pPreviousBtn;
    QPushButton *m_pNextBtn;

    QDataWidgetMapper *m_pMapper;
};
#endif // MAINWINDOW_H

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值