云计算百科
云计算领域专业知识百科平台

【Qt】《Qt 5.9 C++开发指南》笔记-Chapter5-ModelView结构

第5章  Model/View结构

Model/View(模型/视图)结构是Qt中用界面组件显示与编辑数据的一种结构,视图(View)是显示和编辑数据的界面组件,模型(Model)是视图与原始数据之间的接口。Model/View结构的典型应用是在数据库应用程序中,例如数据库中的一个数据表可以在一个QTableView组件中显示和编辑。

主要的视图组件有QListView、QTreeView和QTableView,第4章介绍的QListWidget、QTreeWidget和QTableWidget分别是这3个类的便利类,它们不使用数据模型,而是将数据直接存储在组件的每个项里。

本章介绍Model/View结构原理,包括QListView、QTreeView、QTableView视图组件,以及QStringListModel、QStandardItemModel等模型类的用法。

5.1 Model/View结构

5.1.1 Model/View基本原理

GUI应用程序的一个很重要的功能是由用户在界面上编辑和修改数据,典型的如数据库应用程序。数据库应用程序中,用户在界面上执行各种操作,实际上是修改了界面组件所关联的数据库内的数据。

将界面组件与所编辑的数据分离开来,又通过数据源的方式连接起来,是处理界面与数据的一种较好的方式。Qt使用Model/View结构来处理这种关系,Model/View的基本结构如原书的图5-1所示。其中各部分的功能如下。

  • 数据(Data)是实际的数据,如数据库的一个数据表或SQL查询结果,内存中的一个StringList,或磁盘文件结构等。

  • 视图或视图组件(View)是屏幕上的界面组件,视图从数据模型获得每个数据项的模型索引(model index),通过模型索引获取数据,然后为界面组件提供显示数据。Qt提供一些现成的数据视图组件,如QListView、QTreeView和QTableView等。

  • 模型或数据模型(Model)与实际数据通信,并为视图组件提供数据接口。它从原始数据提取需要的内容,用于视图组件进行显示和编辑。Qt中有一些预定义的数据模型,如QStringListModel可作为StringList的数据模型,QSqlTableModel可以作为数据库中一个数据表的数据模型。

由于数据源与显示界面通过Model/View结构分离开来,因此可以将一个数据模型在不同的视图中显示,也可以在不修改数据模型的情况下,设计特殊的视图组件。

在Model/View结构中,还提供了代理(Delegate)功能,代理功能可以让用户定制数据的界面显示和编辑方式。在标准的视图组件中,代理功能显示一个数据,当数据被编辑时,代理通过模型索引与数据模型通信,并为编辑数据提供一个编辑器,一般是一个QLineEdit组件。

模型、视图和代理之间使用信号和槽通信。当源数据发生变化时,数据模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示这些操作信息;当编辑数据时,代理发射信号告知数据模型和视图组件编辑器的状态。

5.1.2 数据模型

所有的基于项数据(item data)的数据模型(Model)都是基于QAbstractItemModel类的,这个类定义了视图组件和代理存取数据的接口。数据无需存储在数据模型里,数据可以是其他类、文件、数据库或任何数据源。Qt中与数据模型相关的几个主要的类的层次结构如原书的图5-2所示。

图5-2中的抽象类是不能直接使用的,需要由子类继承来实现一些纯虚函数。Qt提供了一些模型类用于项数据处理,常见的几个见表5-1。

表5-1 Qt提供的数据模型类

Model类用途
QStringListModel 用于处理字符串列表数据的数据模型类
QStandardItemModel 标准的基于项数据的数据模型类,每个项数据可以是任何数据类型
QFileSystemModel 计算机上文件系统的数据模型类
QSortFilterProxyModel 与其他数据模型结合,提供排序和过滤功能的数据模型类
QSqlQueryModel 用于数据库SQL查询结果的数据模型类
QSqlTableModel 用于数据库的一个数据表的数据模型类
QSqlRelationalTableModel 用于关系型数据表的数据模型类

数据库相关的3个模型类将在第11章介绍数据库编程时专门说明。如果这些现有的模型类无法满足需求,用户可以从QAbstractItemModel、QAbstractListModel或QAbstractTableModel继承,生成自己定制的数据模型类。

5.1.3 视图组件

视图组件(View)就是显示数据模型的数据的界面组件,Qt提供的视图组件如下。

  • QListView:用于显示单列的列表数据,适用于一维数据的操作。

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

  • QTableView:用于显示表格状数据,适用于二维表格型数据的操作。

  • QColumnView:用多个QListView显示树状层次结构,树状结构的一层用一个QListView显示。

  • QHeaderView:提供行表头或列表头的视图组件,如QTableView的行表头和列表头。

视图组件在显示数据时,只需调用视图类的 setModel()函数,为视图组件设置一个数据模型就可以实现视图组件与数据模型之间的关联,在视图组件上的修改将自动保存到关联的数据模型里,一个数据模型可以同时在多个视图组件里显示数据。

在第4章介绍了QListWidget、QTreeWidget和QtableWidget 3个可用于数据编辑的组件。这3个类称为便利类(convenience classes),它们分别是3个视图类的子类,其层次关系如原书的图5-3所示。

用于Model/View结构的几个视图类直接从QAbstractItemView继承而来,而便利类则从相应的视图类继承而来。

视图组件类的数据采用单独的数据模型,视图组件不存储数据。便利类则为组件的每个节点或单元格创建一个项(item),用项存储数据、格式设置等。所以,便利类没有数据模型,它实际上是用项的方式集成了数据模型的功能,这样就将界面与数据绑定了。所以,便利类缺乏对大型数据源进行灵活处理的能力,适用于小型数据的显示和编辑。

5.1.4 代理

代理(Delegate)就是在视图组件上为编辑数据提供编辑器,如在表格组件中编辑一个单元格的数据时,缺省是使用一个QLineEdit编辑框。代理负责从数据模型获取相应的数据,然后显示在编辑器里,修改数据后,又将其保存到数据模型中。

QAbstractItemDelegate是所有代理类的基类,作为抽象类,它不能直接使用。它的一个子类QStyledItemDelegate,是Qt的视图组件缺省使用的代理类。

对于一些特殊的数据编辑需求,例如只允许输入整型数,使用一个QSpinBox作为代理组件更恰当,从列表中选择数据时使用一个QComboBox作为代理组件更好。这时,就可以从QStyledItemDelegate继承创建自定义代理类。

5.1.5 Model/View结构的一些概念

1.数据模型的基本结构

在Model/View结构中,数据模型为视图组件和代理提供存取数据的标准接口。在Qt中,所有的数据模型类都从QAbstractItemModel继承而来,不管底层的数据结构是如何组织数据的,QAbstractItemModel的子类都以表格的层次结构表示数据,视图组件通过这种规则来存取模型中的数据,但是表现给用户的形式不一样。

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

2.模型索引

为了保证数据的表示与数据存取方式隔离,数据模型中引入了模型索引(model index)的概念。通过数据模型存取的每个数据都有一个模型索引,视图组件和代理都通过模型索引来获取数据。

QModelIndex表示模型索引的类。模型索引提供数据存取的一个临时指针,用于通过数据模型提取或修改数据。因为模型内部组织数据的结构随时可能改变,所以模型索引是临时的。如果需要使用持久性的模型索引,则要使用QPersistentModelIndex类。

3.行号和列号

数据模型的基本形式是用行和列定义的表格数据,但这并不意味着底层的数据是用二维数组存储的,使用行和列只是为了组件之间交互方便的一种规定。通过模型索引的行号和列号就可以存取数据。

要获得一个模型索引,必须提供3个参数:行号、列号、父项的模型索引。例如,对于如原书的图5-4中的表格数据模型中的3个数据项A、B、C,获取其模型索引的代码是:

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

在创建模型索引的函数中需要传递行号、列号和父项的模型索引。对于列表和表格模式的数据模型,顶层节点总是用QModelIndex()表示。

4.父项

当数据模型是列表或表格时,使用行号、列号存储数据比较直观,所有数据项的父项(parent item)就是顶层项;当数据模型是树状结构时,情况比较复杂(树状结构中,项一般习惯于称为节点),一个节点可以有父节点,也可以是其他节点的父节点,在构造数据项的模型索引时,必须指定正确的行号、列号和父节点。

对于原书的图5-4中的树状数据模型,节点A和节点C的父节点是顶层节点,获取模型索引的代码是:

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

但是,节点B的父节点是节点A,节点B的模型索引由下面的代码生成:

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

5.项的角色

在为数据模型的一个项设置数据时,可以赋予其不同项的角色(item role)的数据。例如,数据模型类QStandardItemModel的项数据类是QStandardItem,其设置数据的函数是:

void QStandardItem::setData(const QVariant &value,int role= Qt::UserRole + 1)

其中,value是需要设置的数据,role是设置数据的角色。一个项可以有不同角色的数据,用于不同的场合。

role是Qt::ItemDataRole枚举类型,有多种取值,如Qt::DisplayRole 角色是在视图组件中显示的字符串,Qt::ToolTipRole是鼠标提示消息,Qt::UserRole可以自定义数据。项的标准角色是Qt::DisplayRole。

在获取一个项的数据时也需要指定角色,以获取不同角色的数据。

QVariant QStandardItem::data(int role = Qt::UserRole + 1) const

为一个项的不同角色定义数据,可以告知视图组件和代理组件如何显示数据。例如,在原书的图5-5中,项的DisplayRole数据是显示的字符串,DecorationRole是用于装饰显示的属性,ToolTipRole定义了鼠标提示信息。不同的视图组件对各种角色数据的解释和显示可能不一样,也可能忽略某些角色的数据。

第4章已经介绍了便利类QListWidget、QTree Widget和QTableWidget的使用,本章将介绍Model/ View结构的基本用法,包括Qt预定义的Qstring ListModel、QFileSystemModel、QStandardItemModel以及视图组件QListView、QTableView、QTreeView的使用,还介绍如何设计和使用自定义代理。涉及数据库的Model/View的使用将在数据库一章单独介绍。

5.2 QFileSystemModel

5.2.1 QFileSystemModel类的基本功能

QFileSystemModel提供了一个可用于访问本机文件系统的数据模型。QFileSystemModel和视图组件QTreeView结合使用,可以用目录树的形式显示本机上的文件系统,如同Windows的资源管理器一样。使用QFileSystemModel提供的接口函数,可以创建目录、删除目录、重命名目录,可以获得文件名称、目录名称、文件大小等参数,还可以获得文件的详细信息。

要通过QFileSystemModel获得本机的文件系统,需要用setRootPath()函数为QFileSystemModel设置一个根目录,例如:

QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());

静态函数QDir::currentPath()获取应用程序的当前路径。

用于获取磁盘文件目录的数据模型类还有一个QDirModel,QDirModel的功能与QFileSystemModel类似,也可以获取目录和文件,但是QFileSystemModel采用单独的线程获取目录文件结构,而QDirModel不使用单独的线程。使用单独的线程就不会阻碍主线程,所以推荐使用QFileSystemModel。

使用QFileSystemModel作为数据模型,QTreeView、QListView和QTableView为主要组件设计的实例samp5_1运行界面如原书的图5-6所示。在TreeView中以目录树的形式显示本机的文件系统,单击一个目录时,右边的ListView和TableView显示该目录下的目录和文件。在TreeView上单击一个目录或文件节点时,下方的几个标签里显示当前节点的信息。

5.2.2 QFileSystemModel的使用

实例samp5_1的主窗口是基于QMainWindow的,在使用UI设计器做可视化设计时删除了工具栏和状态栏。主窗口界面布局采用了两个分割条的设计,ListView和TableView采用上下分割布局,然后和左边的TreeView采用水平分割布局,水平分割布局再和下方显示信息的groupBox在主窗口工作区水平布局。

在主窗口类中定义了一个QFileSystemModel类的成员变量model。

QFileSystemModel *model;

主窗口构造函数进行初始化,代码如下:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
model=new QFileSystemModel(this);
model->setRootPath(QDir::currentPath()); //设置根目录
ui->treeView->setModel(model); //设置数据模型
ui->listView->setModel(model); //设置数据模型
ui->tableView->setModel(model); //设置数据模型
//信号与槽关联,treeView单击时,其目录设置为listView和tableView的根节点
connect(ui->treeView,SIGNAL(clicked(QModelIndex)),
ui->listView,SLOT(setRootIndex(QModelIndex)));
connect(ui->treeView,SIGNAL(clicked(QModelIndex)),
ui->tableView,SLOT(setRootIndex(QModelIndex)));
}

3个视图组件都使用setModel()函数,将QFileSystemModel数据模型model设置为自己的数据模型。

connect()函数设置信号与槽的关联,实现的功能是:在单击treeView的一个节点时,此节点就设置为listView和tableView的根节点,因为treeView的clicked(QModelIndex)信号会传递一个QModelIndex变量,是当前节点的模型索引,将此模型索引传递给listView和tableView的槽函数setRootIndex(QModelIndex),listView和tableView就会显示此节点下的目录和文件。

在treeView上单击一个节点时,下方的一些标签里会显示节点的一些信息,这是为treeView的clicked(const QModelIndex &index)信号编写槽函数实现的,其代码如下:

void MainWindow::on_treeView_clicked(const QModelIndex &index)
{
ui->chkIsDir->setChecked(model->isDir(index)); //是否是目录
ui->LabPath->setText(model->filePath(index));
ui->LabType->setText(model->type(index));
ui->LabFileName->setText(model->fileName(index));
int sz=model->size(index)/1024;
if (sz<1024)
ui->LabFileSize->setText(QString("%1 KB").arg(sz));
else
ui->LabFileSize->setText(QString::asprintf("%.1f MB",sz/1024.0));
}

函数有一个传递参数QModelIndex &index,它是单击节点在数据模型中的索引。通过传递来的模型索引index,这段代码使用了QFileSystemModel的一些函数来获得节点的一些参数,包括以下几种。

  • bool isDir(QModelIndex &index):判断节点是不是一个目录。

  • QString filePath(QModelIndex &index):返回节点的目录名或带路径的文件名。

  • QString fileName(QModelIndex &index):返回去除路径的文件夹名称或文件名。

  • QString type(QModelIndex &index):返回描述节点类型的文字,如硬盘符是“Drive”,文件夹是“File Folder”,文件则用具体的后缀描述,如“txt File”“exe File”“pdf File”等。

  • qint64 size(QModelIndex &index):如果节点是文件,返回文件大小的字节数:如果节点是文件夹,返回0。

而QFileSystemModel是如何获取磁盘目录文件结构的,3个视图组件是如何显示这些数据的,则是其底层实现的问题了。

5.3 QStringListModel

5.3.1 QStringListModel功能概述

QStringListModel用于处理字符串列表的数据模型,它可以作为QListView的数据模型,在界面上显示和编辑字符串列表。

QStringListModel的setStringList()函数可以初始化数据模型的字符串列表的内容,stringList()函数返回数据模型内的字符串列表,在关联的ListView组件里编辑修改数据后,数据都会及时更新到数据模型内的字符串列表里。

QStringListModel提供编辑和修改字符串列表数据的函数,如insertRows()、removeRows()、setData()等,这些操作直接影响数据模型内部的字符串列表,并且修改后的数据会自动在关联的ListView组件里刷新显示。

实例samp5_2采用QStringListModel作为数据模型,QListView组件作为视图组件,演示了QStringListModel和QListView构成Model/View结构编辑字符串列表的功能,程序运行时界面如原书的图5-7所示。

窗口左侧是对QStringListModel的一些操作,右侧的QPlaintextEdit组件显示QString ListModel:: stringList()的内容,以查看其是否与界面上ListView组件显示的内容一致。

5.3.2 QStringListModel的使用

1.Model/View结构对象和组件初始化

实例samp5_2的窗口是从QWidget继承而来的类Widget,界面采用可视化设计。在Widget类中定义一个QStringListModel类的变量:

QStringListModel *theModel;

在Widget类的构造函数中进行变量的创建,完成数据模型与界面视图组件的关联,下面是Widget类构造函数的代码:

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
ui->setupUi(this);
QStringList theStrList;
theStrList<<"北京"<<"上海"<<"天津"<<"河北"<<"山东"<<"四川"<<"重庆";
theModel=new QStringListModel(this);
theModel->setStringList(theStrList); //导入theStrList的内容
ui->listView->setModel(theModel); //设置数据模型
ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::SelectedClicked);
}

QStringListModel的setStringList()函数将一个字符串列表的内容作为数据模型的初始数据内容。

QListView的setModel()函数为界面视图组件设置一个数据模型。

程序运行后,界面上ListView组件里就会显示初始化的字符串列表的内容。

2.编辑、添加、删除项的操作
  • 编辑项

QListView::setEditTriggers()函数设置QListView的条目是否可以编辑,以及如何进入编辑状态,函数的参数是QAbstractItemView::EditTrigger枚举类型值的组合。构造函数中设置为:

ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::SelectedClicked);

表示在双击,或选择并单击列表项后,就进入编辑状态。

若要设置为不可编辑,则可以设置为:

ui->listView->setEditTriggers(QAbstractItemView:: NoEditTriggers);

  • 添加项

添加项是要在列表的最后添加一行,界面上“添加项”按钮的槽函数代码如下:

void Widget::on_btnListAppend_clicked()
{ //添加一行
theModel->insertRow(theModel->rowCount()); //在尾部插入一空行
QModelIndex index=theModel->index(theModel->rowCount()-1,0);
theModel->setData(index,"new item",Qt::DisplayRole);
ui->listView->setCurrentIndex(index); //设置当前选中的行
}

对数据的操作都是针对数据模型的,所以,插入一行使用的是QStringListModel的insertRow (int row)函数,其中row是一个行号,表示在row行之前插入一行。要在列表的最后插入一行,参数row设置为列表当前的行数即可。

这样只是在列表尾部添加一个空行,没有任何文字。为了给添加的项设置一个缺省的文字标题,首先要获得新增项的模型索引,即:

QModelIndex index=theModel->index(theModel->rowCount()-1,0);

QStringListModel的index()函数根据传递的行号、列号、父项的模型索引生成一个模型索引,这行代码为新增的最后一个项生成一个模型索引index。

为新增的项设置一个文字标题“new item”,使用setData()函数,并用到前面生成的模型索引index。代码如下:

theModel->setData(index,"new item",Qt::DisplayRole);

在使用setData()函数时,必须指定设置数据的角色,这里的角色是Qt::DisplayRole,它是用于显示的角色,即项的文字标题。

  • 插入项

“插入项”按钮的功能是在列表的当前行前面插入一行,其实现代码如下:

void Widget::on_btnListInsert_clicked()
{//插入一行
QModelIndex index=ui->listView->currentIndex();
theModel->insertRow(index.row());
theModel->setData(index,"inserted item",Qt::DisplayRole);
ui->listView->setCurrentIndex(index);
}

QListView::currentIndex()获得当前项的模型索引index,index.row()则返回这个模型索引的行号。

  • 删除当前项

使用QStringListModel的removeRow()函数删除某一行的代码如下:

void Widget::on_btnListDelete_clicked()
{//删除当前行
QModelIndex index=ui->listView->currentIndex();
theModel->removeRow(index.row());
}

  • 删除列表

删除列表的所有项可使用QStringListModel的removeRows(int row, int count)函数,它表示从行号row开始删除count行。代码如下:

void Widget::on_btnListClear_clicked()
{//清除所有项
theModel->removeRows(0,theModel->rowCount());
}

3.以文本显示数据模型的内容

以上在对界面上ListView的项进行编辑时,实际操作的都是其关联的数据模型theModel,在对数据模型进行插入、添加、删除项操作后,内容立即在ListView上显示出来,这是数据模型与视图组件之间信号与槽的作用,当数据模型的内容发生改变时,通知视图组件更新显示。

同样的,当在ListView上双击一行进入编辑状态,修改一个项的文字内容后,这部分内容也保存到数据模型里了,这就是原书的图5-1所表示的过程。

那么,数据模型内部应该保存有最新的数据内容,对于QStringListModel模型来说,通过stringList()函数可以得到其最新的数据副本。界面上的“显示数据模型的StringList”按钮获取数据模型的stringList,并用多行文本的形式显示其内容,以检验对数据模型修改数据,特别是在界面上修改列表项的文字后,其内部的数据是否同步更新了。

以下是界面上的“显示数据模型的StringList”按钮的clicked()信号的槽函数代码,它通过数据模型的stringList()函数获取字符串列表,并在plainTextEdit里逐行显示:

void Widget::on_btnTextImport_clicked()
{//显示数据模型的StringList
QStringList tmpList=theModel->stringList();
ui->plainTextEdit->clear();
for (int i=0; i<tmpList.count();i++)
ui->plainTextEdit->appendPlainText(tmpList.at(i));
}

程序运行时,无论对ListView的列表做了什么编辑和修改,单击“显示数据模型的StringList”按钮,在文本框里显示的文字内容与ListView里总是完全相同的,说明数据模型的数据与界面上显示的内容是同步的。

4.其他功能

QListView的clicked()信号会传递一个QModelIndex类型的参数,利用该参数,可以显示当前项的模型索引的行和列的信息,实现代码如下:

void Widget::on_listView_clicked(const QModelIndex &index)
{ //显示QModelIndex的行、列号
ui->LabInfo->setText(QString::asprintf("当前条目:row=%d, column=%d",
index.row(),index.column()));
}

在这个实例中,通过QStringListModel和QListView说明了数据模型与视图组件之间构成Model/View结构的基本原理。

第4章的实例samp4_7中采用QListWidget设计了一个列表编辑器,对比这两个实例,可以发现如下两点。

  • 在Model/View结构中,数据模型与视图组件是分离的,可以直接操作数据模型以修改数据,在视图组件中做的修改也会自动保存到数据模型里。

  • 在使用QListWidget的例子中,每个列表项是一个QListWidgetItem类型的变量,保存了项的各种数据,数据和显示界面是一体的,对数据的修改操作就是对项关联的变量的修改。

所以,这是Model/View结构与便利组件之间的主要区别。

5.4 QStandardItemModel

5.4.1 功能概述

QStandardItemModel是标准的以项数据(item data)为基础的标准数据模型类,通常与QTableView组合成Model/View结构,实现通用的二维数据的管理功能。

本节介绍QStandardItemModel的使用,主要用到以下3个类。

  • QStandardItemModel:基于项数据的标准数据模型,可以处理二维数据。维护一个二维的项数据数组,每个项是一个QStandardItem类的变量,用于存储项的数据、字体格式、对齐方式等。

  • QTableView:二维数据表视图组件,有多个行和多个列,每个基本显示单元是一个单元格,通过setModel()函数设置一个QStandardItemModel类的数据模型之后,一个单元格显示QStandardItemModel数据模型中的一个项。

  • QItemSelectionModel:一个用于跟踪视图组件的单元格选择状态的类,当在QTableView选择某个单元格,或多个单元格时,通过QItemSelectionModel可以获得选中的单元格的模型索引,为单元格的选择操作提供方便。

这几个类之间的关系是:QTableView是界面视图组件,其关联的数据模型是QStandardItem Model,关联的项选择模型是QItemSelectionModel,QStandardItemModel的数据管理的基本单元是QStandardItem。

实例samp5_3演示QStandardItemModel的使用,其运行时界面如原书的图5-8所示。

该实例具有如下功能。

  • 打开一个纯文本文件,该文件是规则的二维数据文件,通过字符串处理获取表头和各行各列的数据,导入到一个QStandardItemModel数据模型。

  • 编辑修改数据模型的数据,可以插入行、添加行、删除行,还可以在QTableView视图组件中直接修改单元格的数据内容。

  • 可以设置数据模型中某个项的不同角色的数据,包括文字对齐方式、字体是否粗体等。

  • 通过QItemSelectionModel获取视图组件上的当前单元格,以及选择单元格的范围,对选择的单元格进行操作。

  • 将数据模型的数据内容显示到QPlainTextEdit组件里,显示数据模型的内容,检验视图组件上做的修改是否与数据模型同步。

  • 将修改后的模型数据另存为一个文本文件。

5.4.2 界面设计与主窗口类定义

本实例的主窗口从QMainWindow继承而来,中间的TableView和PlainTextEdit组件采用水平分割条布局。在Action编辑器中创建如原书的图5-9所示的一些Action,并由Action创建主工具栏上的按钮,下方的状态栏设置了几个QLabel组件,显示当前文件名称、当前单元格行号、列号,以及相应内容。

主窗口类MainWindow里新增的定义如下(省略了UI设计器生成的界面组件的槽函数的声明):

#define FixedColumnCount 6 //文件固定6列
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabCurFile; //当前文件
QLabel *LabCellPos; //当前单元格行列号
QLabel *LabCellText; //当前单元格内容
QStandardItemModel *theModel; //数据模型
QItemSelectionModel *theSelection;//选择模型
void iniModelFromStringList(QStringList&);//从StringList初始化数据模型
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//当前选择单元格发生变化
void on_currentChanged(const QModelIndex &current, const QModelIndex &previous);
private:
Ui::MainWindow *ui;
};

这里定义了数据模型变量theModel,项数据选择模型变量theSelection。

定义的私有函数iniModelFromStringList()用于在打开文件时,从一个QStringList变量的内容创建数据模型。

自定义槽函数on_currentChanged()用于在TableView上选择单元格发生变化时,更新状态栏的信息显示,这个槽函数将会与项选择模型theSelection的currentChanged()信号关联。

5.4.3 QStandardItemModel的使用

1.系统初始化

在MainWindow的构造函数中进行界面初始化,数据模型和选择模型的创建,以及与视图组件的关联,信号与槽的关联等设置,代码如下:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
setCentralWidget(ui->splitter);
theModel = new QStandardItemModel(2,FixedColumnCount,this); //数据模型
theSelection = new QItemSelectionModel(theModel);//选择模型
connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
ui->tableView->setModel(theModel); //设置数据模型
ui->tableView->setSelectionModel(theSelection);//设置选择模型
ui->tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectItems);
//创建状态栏组件,代码略
}

在构造函数里首先创建数据模型theModel,创建数据选择模型时需要传递一个数据模型变量作为其参数。这样,数据选择模型theSelection就与数据模型theModel关联,用于表示theModel的项数据选择操作。

创建数据模型和选择模型后,为TableView组件设置数据模型和选择模型:

ui->tableView->setModel(theModel); //设置数据模型
ui->tableView->setSelectionModel(theSelection);//设置选择模型

构造函数里还将自定义的槽函数on_currentChanged()与theSelection的currentChanged()信号关联,用于界面上tableView选择单元格发生变化时,显示单元格的行号、列号、内容等信息,槽函数代码如下:

void MainWindow::on_currentChanged(const QModelIndex &current, const QModelIndex &previous)
{ //选择单元格变化时的响应
if (current.isValid())
{
LabCellPos->setText(QString::asprintf("当前单元格:%d行,%d列",
current.row(),current.column()));
QStandardItem* aItem=theModel->itemFromIndex(current);
this->LabCellText->setText("单元格内容:"+aItem->text());
QFont font=aItem->font();
ui->actFontBold->setChecked(font.bold());
}
}

2.从文本文件导入数据

QStandardItemModel是标准的基于项数据的数据模型,以类似于二维数组的形式管理内部数据,适合于处理表格型数据,其显示一般采用QTableView。

QStandardItemModel的数据可以是程序生成的内存中的数据,也可以来源于文件。例如,在实际数据处理中,有些数据经常是以纯文本格式保存的,它们有固定的列数,每一列是一项数据,实际构成一个二维数据表。原书的图5-10是本实例程序要打开的一个纯文本文件的内容,文件的第1行是数据列的文字标题,相当于数据表的表头,然后以行存储数据,以TAB键间隔每列数据。

当单击工具栏上的“打开文件”按钮时,需要选择一个这样的文件导入到数据模型,并在tableView上进行显示和编辑。图5-10的数据有6列,第1列是整数,第2至4列是浮点数,第5列是文字,第6列是逻辑型变量,“1”表示true。

下面是“打开文件”按钮的槽函数代码:

void MainWindow::on_actOpen_triggered()
{ //打开文件
QString curPath=QCoreApplication::applicationDirPath();
QString aFileName=QFileDialog::getOpenFileName(this,"打开一个文件",
curPath,"井数据文件(*.txt);;所有文件(*.*)");
if (aFileName.isEmpty())
return;

QStringList fFileContent;
QFile aFile(aFileName);
if (aFile.open(QIODevice::ReadOnly | QIODevice::Text)) //打开文件
{
QTextStream aStream(&aFile); //用文本流读取文件
ui->plainTextEdit->clear();
while (!aStream.atEnd())
{
QString str=aStream.readLine();
ui->plainTextEdit->appendPlainText(str);
fFileContent.append(str);
}
aFile.close();
this->LabCurFile->setText("当前文件:"+aFileName);//状态栏显示
iniModelFromStringList(fFileContent);//初始化数据模型
}
}

这段代码让用户选择所需要打开的数据文本文件,然后用只读和文本格式打开文件,逐行读取其内容,将每行字符串显示到界面上的plainTextEdit里,并且添加到一个临时的QStringList类型的变量fFileContent里。

然后调用自定义函数iniModelFromStringList(),用fFileContent的内容初始化数据模型。下面是iniModelFromStringList()函数的代码:

void MainWindow::iniModelFromStringList(QStringList& aFileContent)
{ //从一个StringList 获取数据,初始化数据模型
int rowCnt=aFileContent.count(); //文本行数,第1行是标题
theModel->setRowCount(rowCnt-1);
//设置表头,一个或多个空格、TAB等分隔符隔开的字符串,分解为一个StringList
QString header=aFileContent.at(0);//第1行是表头
QStringList headerList=
header.split(QRegExp("\\\\s+"),QString::SkipEmptyParts);
theModel->setHorizontalHeaderLabels(headerList); //设置表头文字
//设置表格数据
QStandardItem *aItem;
QStringList tmpList;
int j;
for (int i=1;i<rowCnt;i++)
{
QString aLineText=aFileContent.at(i);
tmpList=aLineText.split(QRegExp("\\\\s+"),QString::SkipEmptyParts);
for (j=0;j<FixedColumnCount-1;j++)
{ //不包含最后一列
aItem=new QStandardItem(tmpList.at(j));
theModel->setItem(i-1,j,aItem); //为模型的某个行列位置设置Item
}
aItem=new QStandardItem(headerList.at(j));//最后一列
aItem->setCheckable(true); //设置为Checkable
if (tmpList.at(j)=="0")
aItem->setCheckState(Qt::Unchecked);
else
aItem->setCheckState(Qt::Checked);
theModel->setItem(i-1,j,aItem);
}
}

传递来的参数aFileContent是文本文件所有行构成的StringList,文件的每一行是aFileContent的一行字符串,第1行是表头文字,数据从第2行开始。

程序首先获取字符串列表的行数,然后设置数据模型的行数,因为数据模型的列数在初始化时已经设置了。

然后获取字符串列表的第1行,即表头文字,用QString::split()函数分割成一个QStringList,设置为数据模型的表头标题。

QString::split()函数根据某个特定的符号将字符串进行分割。例如,header是数据列的标题,每个标题之间通过一个或多个TAB键分隔,其内容是:

测深(m) 垂深(m) 方位(°) 总位移(m) 固井质量 测井取样

那么通过上面的split()函数操作,得到一个字符串列表headerList,其内容是:

测深(m)
垂深(m)
方位(°)
总位移(m)
固井质量
测井取样

也就是分解为一个6行的StringList。然后使用此字符串列表作为数据模型,设置表头标题的函数setHorizontalHeaderLabels()的参数,就可以为数据模型设置表头了。

同样,在逐行获取字符串后,也采用split()函数进行分解,为每个数据创建一个QStandardItem类型的项数据aItem,并赋给数据模型作为某行某列的项数据。

QStandardItemModel以二维表格的形式保存项数据,每个项数据对应着QTableView的一个单元格。项数据不仅可以存储显示的文字,还可以存储其他角色的数据。

数据文件的最后一列是一个逻辑型数据,在tableView上显示时为其提供一个CheckBox组件,此功能通过调用QStandardItem的setCheckable()函数实现。

3.数据修改

当TableView设置为可编辑时,双击一个单元格可以修改其内容,对于使用CheckBox的列,改变CheckBox的勾选状态,就可以修改单元格关联项的选择状态。

在实例主窗口工具栏上有“添加行”“插入行”“删除行”按钮,它们实现相应的编辑操作,这些操作都是直接针对数据模型的,数据模型被修改后,会直接在TableView上显示出来。

  • 添加行

“添加行”操作是在数据表的最后添加一行,其实现代码如下:

void MainWindow::on_actAppend_triggered()
{ //在表格最后添加行
QList<QStandardItem*> aItemList; //列表类
QStandardItem *aItem;
for(int i=0;iheaderData(theModel->columnCount()-1, Qt::Horizontal, Qt::DisplayRole).toString();
aItem=new QStandardItem(str); //创建 "测井取样" Item
aItem->setCheckable(true);
aItemList<insertRow(theModel->rowCount(),aItemList); //插入一行
QModelIndex curIndex=theModel->index(theModel->rowCount()-1,0);
theSelection->clearSelection();
theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}

使用QStandardItemModel::insertRow()函数插入一行,其函数原型是:

void insertRow(int row, const QList<QStandardItem *> &items)

其中,row是一个行号,表示在此行号之前插入一行,若row等于或大于总行数,则在最后添加一行。QList<QStandardItem *> &items 是一个QStandardItem类型的列表类,需要为插入的一行的每个项数据创建一个QStandardItem类型的项,然后传递给insertRow()函数。

在这段程序中,为前5列创建QStandardItem对象时,都使用文字“0”,最后一列使用表头的标题,并设置为Checkable。创建完每个项数据对象后,使用insertRow()函数在最后添加一行。

  • 插入行

“插入行”按钮的功能是在当前行的前面插入一行,实现代码与“添加行”类似。

  • 删除行

“删除行”按钮的功能是删除当前行,首先从选择模型中获取当前单元格的模型索引,然后从模型索引中获取行号,调用removeRow(int row)删除指定的行。

void MainWindow::on_actDelete_triggered()
{ //删除行
QModelIndex curIndex=theSelection->currentIndex();//获取模型索引
if (curIndex.row()==theModel->rowCount()-1)//最后一行
theModel->removeRow(curIndex.row()); //删除最后一行
else
{
theModel->removeRow(curIndex.row());//删除一行,并重新设置当前选择行
theSelection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}
}

4.单元格格式设置

工具栏上有3个设置单元格文字对齐方式的按钮,还有一个设置字体粗体的按钮。当在TableView中选择多个单元格时,可以同时设置多个单元格的格式。例如,“居左”按钮的代码如下:

void MainWindow::on_actAlignLeft_triggered()
{//设置文字居左对齐
if (!theSelection->hasSelection())
return;
//获取选择的单元格的模型索引列表,可以是多选
QModelIndexList selectedIndex=theSelection->selectedIndexes();
for (int i=0;i<selectedIndex.count();i++)
{
QModelIndex aIndex=selectedIndex.at(i); //获取一个模型索引
QStandardItem* aItem=theModel->itemFromIndex(aIndex);
aItem->setTextAlignment(Qt::AlignLeft);//设置文字对齐方式
}
}

QItemSelectionModel::selectedIndexes()函数返回选择单元格的模型索引列表,然后通过此列表获取每个选择的单元格的模型索引,再通过模型索引获取其项数据,然后调用QStandardItem::setTextAlignment()设置一个项的对齐方式即可。

“居中”和“居右”按钮的代码与此类似。

“粗体”按钮设置单元格的字体是否为粗体,在选择单元格时,actFontBold的check状态根据当前单元格的字体是否为粗体自动更新。actFontBold的triggered(bool)的槽函数代码如下,与设置对齐方式的代码操作方式类似:

void MainWindow::on_actFontBold_triggered(bool checked)
{//设置字体粗体
if (!theSelection->hasSelection())
return;
QModelIndexList selectedIndex=theSelection->selectedIndexes();
for (int i=0;i< selectedIndex.count();i++)
{
QModelIndex aIndex= selectedIndex.at(i); //获取一个模型索引
QStandardItem* aItem=theModel->itemFromIndex(aIndex);//获取项数据
QFont font=aItem->font();
font.setBold(checked); //设置字体是否粗体
aItem->setFont(font);
}
}

5.数据另存为文件

在视图组件上对数据的修改都会自动更新到数据模型里,单击工具栏上的“模型数据预览”按钮,可以将数据模型的数据内容显示到PlainTextEdit里。

数据模型里的数据是在内存中的,工具栏上的“另存文件”按钮可以将数据模型的数据另存为一个数据文本文件,同时也显示在PlainTextEdit里,其实现代码如下:

void MainWindow::on_actSave_triggered()
{ //保存为文件
QString curPath=QCoreApplication::applicationDirPath();
QString aFileName=QFileDialog::getSaveFileName(this,"选择一个文件", curPath,"井斜数据文件(*.txt);;所有文件(*.*)");
if (aFileName.isEmpty())
return;
QFile aFile(aFileName);
if (!(aFile.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)))
return; //以读写、覆盖原有内容方式打开文件

QTextStream aStream(&aFile);
QStandardItem *aItem;
int i,j;
QString str;
ui->plainTextEdit->clear();
//获取表头文字
for (i=0;i<theModel->columnCount();i++)
{ aItem=theModel->horizontalHeaderItem(i); //获取表头的项数据
str=str+aItem->text()+"\\t\\t"; //以TAB隔开
}
aStream<plainTextEdit->appendPlainText(str);
//获取数据区文字
for ( i=0;i<theModel->rowCount();i++)
{ str="";
for( j=0;j<theModel->columnCount()-1;j++)
{ aItem=theModel->item(i,j);
str=str+aItem->text()+QString::asprintf("\\t\\t");
}
aItem=theModel->item(i,j); //最后一列是逻辑型
if (aItem->checkState()==Qt::Checked)
str=str+"1";
else
str=str+"0";
ui->plainTextEdit->appendPlainText(str);
aStream<

5.5 自定义代理

5.5.1 自定义代理的功能

在前一节的实例samp5_3中,导入数据文件进行编辑时,QTableView组件为每个单元格提供的是缺省的代理编辑组件,就是一个QLineEdit组件。在编辑框里可以输入任何数据,所以比较通用。但是有些情况下,希望根据数据的类型限定使用不同的编辑组件,例如在samp5_3的实例的数据中,第1列“测深”是整数,使用QSpinBox作为编辑组件更合适;“垂深”“方位”“总位移”是浮点数,使用QDoubleSpinBox更合适;而“固井质量”使用一个QComboBox,从一组列表文字中选择更合适。

要实现这些功能,就需要为TableView的某列或某个单元格设置自定义代理组件。本节在实例samp5_3的基础上,为TableView增加自定义代理组件功能。设定自定义代理组件之后的TableView运行时,其编辑状态的效果如原书的图5-11所示。

5.5.2 自定义代理类的基本设计要求

Qt中有关代理的几个类的层次结构如原书的图5-12所示。

QAbstractItemDelegate是所有代理类的抽象基类,QStyledItemDelegate是视图组件使用的缺省的代理类,QItemDelegate也是类似功能的类。QStyledItemDelegate与QItemDelegate的差别在于:QStyledItemDelegate可以使用当前的样式表设置来绘制组件,因此建议使用QStyledItemDelegate作为自定义代理组件的基类。

不管从QStyledItemDelegate还是QItemDelegate继承设计自定义代理组件,都必须实现如下的4个函数:

  • createEditor()函数创建用于编辑模型数据的widget组件,如一个QSpinBox组件,或一个QComboBox组件;
  • setEditorData()函数从数据模型获取数据,供widget组件进行编辑;
  • setModelData()将widget上的数据更新到数据模型;
  • updateEditorGeometry()用于给widget组件设置一个合适的大小。

5.5.3 基于QSpinBox的自定义代理类

1.自定义代理类的基本结构

下面设计一个基于QSpinBox类的自定义代理类,用于“测深”数据列的编辑。

在Qt Creator里单击“File”→“New File or Project”菜单项,在出现的“New File or Project”对话框里选择新建一个C++class文件,在出现的对话框里,输入自定义类的名称为QWIntSpinDelegate,设置基类为QStyledItemDelegate,单击下一步后结束向导,系统会自动生成头文件和源文件,并添加到项目里。

在头文件qwintspindelegate.h中包含对自定义类QWIntSpinDelegate的定义,在其中添加4个需要重定义的函数的定义,qwintspindelegate.h的内容如下:

#include <QStyledItemDelegate>
class QWIntSpinDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
QWIntSpinDelegate(QObject *parent=0);
//自定义代理组件必须继承以下4个函数
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void setEditorData(QWidget *editor,
const QModelIndex &index)const Q_DECL_OVERRIDE;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const Q_DECL_OVERRIDE;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem
&option, const QModelIndex &index) const Q_DECL_OVERRIDE;
};

自定义代理组件必须重新实现这4个函数,函数的原型都是固定的。

2.createEditor()函数的实现

createEditor()函数用于创建需要的编辑组件,QWIntSpinDelegate类希望创建一个QSpinBox作为编辑组件,函数的实现如下:

QWidget *QWIntSpinDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option, const QModelIndex &index) const
{ //创建代理编辑组件
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false); //设置为无边框
editor->setMinimum(0);
editor->setMaximum(10000);
return editor; //返回此编辑器
}

这段代码创建了一个QSpinBox类型的编辑器editor,parent指向视图组件;然后对创建的editor做一些设置,将editor作为函数的返回值。

3.setEditorData()函数

setEditorData()函数用于从数据模型获取数值,设置为编辑器的显示值。当双击一个单元格进入编辑状态时,就会自动调用此函数,其实现代码如下:

void QWIntSpinDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{//从数据模型获取数据,显示到代理组件中
int value = index.model()->data(index, Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);
}

函数传递来的参数editor指向代理编辑组件,index是关联的数据单元的模型索引。

通过强制类型转换将editor转换为QSpinBox类型组件spinBox,然后将获取的数值设置为spinBox的值。

4.setModelData()函数

setModelData()函数用于将代理编辑器上的值更新给数据模型,当用户在界面上完成编辑时会自动调用此函数,将界面上的数据更新到数据模型。其代码如下:

void QWIntSpinDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{ //将代理组件的数据保存到数据模型中
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText();
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
}

程序先获取代理组件编辑器里的数值,然后利用传递来的数据模型model和模型索引参数index将编辑器的最新值更新到数据模型里。

5.updateEditorGeometry()函数

updateEditorGeometry()函数用于为代理组件设置一个合适的大小,函数传递的参数option的rect变量定义了单元格适合显示代理组件的大小,直接设置为此值即可。代码如下。

void QWIntSpinDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{ //设置组件大小
editor->setGeometry(option.rect);
}

5.5.4 自定义代理类的使用

同样的,可以创建基于QDoubleSpinBox的自定义代理组件类QWFloatSpinDelegate,用于编辑浮点数,还可以创建基于QComboBox的自定义组件类QWComboBoxDelegate。在主窗口的类定义中定义3个代理类的实例变量(省略了其他定义内容):

class MainWindow : public QMainWindow
{
private:
QWIntSpinDelegate intSpinDelegate; //整型数
QWFloatSpinDelegate floatSpinDelegate; //浮点数
QWComboBoxDelegate comboBoxDelegate; //列表选择
}

在MainWindow的构造函数中,为tableView的某些列设置自定义代理组件。增加了自定义代理组件的构造函数代码如下(去掉了初始化状态栏等一些不重要的内容):

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
theModel = new QStandardItemModel(2,FixedColumnCount,this);
theSelection = new QItemSelectionModel(theModel);//选择模型
connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),
this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
ui->tableView->setModel(theModel); //设置数据模型
ui->tableView->setSelectionModel(theSelection);//设置选择模型
//为各列设置自定义代理组件
ui->tableView->setItemDelegateForColumn(0,&intSpinDelegate);//测深
ui->tableView->setItemDelegateForColumn(1,&floatSpinDelegate);//浮点数
ui->tableView->setItemDelegateForColumn(2,&floatSpinDelegate); //浮点数
ui->tableView->setItemDelegateForColumn(3,&floatSpinDelegate); //浮点数
ui->tableView->setItemDelegateForColumn(4,&comboBoxDelegate); //列表
}

为TableView的某一列设置自定义代理组件,使用setItemDelegateForColumn()函数;为某一行设置自定义代理组件,可使用setItemDelegateForRow()函数;若为整个TableView设置一个自定义代理组件,则调用setItemDelegate()函数。

如此增加了自定义代理功能后,在编辑“测深”列时,会在单元格的位置出现一个SpinBox组件,用于输入整数;在编辑第2至4列的浮点数时,会出现一个DoubleSpinBox组件,用于输入浮点数;而在编辑“固井质量”列时,会自动出现一个ComboBox组件,用于从下拉列表中选择一个字符串。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Qt】《Qt 5.9 C++开发指南》笔记-Chapter5-ModelView结构
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!