Model/View结构
将界面组件与所编辑的数据分离开来,又通过数据源的方式连接起来,是处理界面与数据的一种较好的方式。Qt使用Model/View结构来处理这种关系,Model/View的基本结构如图5-1所示。其中各部分的功能如下。
图1 Model/View基本结构(来自Qt帮助文件)
别人写的哪些都特别的麻烦,都不知道要讲什么意思,简单点讲。就是用了Model/View结构之后,我修改了界面上的数据就是同步修改了我Model中的数据,就是两者的数据是同步的。如果我们没有使用Model/View的结构,那么我们修改了界面上的组件的数据之后,只是我的组件中数据修改了,但是我给组件的原始数据是没有修改的,因此我们还得遍历组件的数据,让数据进行同步的处理,才可以。使用这个结构的好处就是,修改数据就是修改同步的数据。
在Model/View结构中,还提供了代理(Delegate)功能,代理功能可以让用户定制数据的界面显示和编辑方式。在标准的视图组件中,代理功能显示一个数据,当数据被编辑时,代理通过模型索引与数据模型通信,并为编辑数据提供一个编辑器,一般是一个QLineEdit组件。
模型、视图和代理之间使用信号和槽通信。当源数据发生变化时,数据模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示这些操作信息;当编辑数据时,代理发射信号告知数据模型和视图组件编辑器的状态。
知道了Model/View是用来干嘛的。使用过程中我们需要理解的原理。
1.数据模型的基本结构
在Model/View结构中,数据模型为视图组件和代理提供存取数据的标准接口。在Qt中,所有的数据模型类都从QAbstractItemModel继承而来,不管底层的数据结构是如何组织数据的,QAbstractItemModel的子类都以表格的层次结构表示数据,视图组件通过这种规则来存取模型中的数据,但是表现给用户的形式不一样。
图是数据模型的3种常见表现形式。不管数据模型的表现形式是怎么样的,数据模型中存储数据的基本单元都是项(item),每个项有一个行号、一个列号,还有一个父项(parent item)。在列表和表格模式下,所有的项都有一个相同的顶层项(root item);在树状结构中,行号、列号、父项稍微复杂一点,但是由这3个参数完全可以定义一个项的位置,从而存取项的数据。
图 数据模型的几种表现形式(来自Qt帮助文件)
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定义了鼠标提示信息。不同的视图组件对各种角色数据的解释和显示可能不一样,也可能忽略某些角色的数据。
图 不同角色数据的表现形式(来自Qt帮助文件)
使用Qt给我们已经准备好的model模型类
Qt中是已经帮我们写好了一些模型类的。我们可以与组件View进行组合就可以实现,model中的数据与组件View中的数据进行一个同步的结果。
使用Model/View的一般的步骤:
1.先建立自己我需要的xxxxModel类。
2.给xxxModel设置初始化,比如增加数据,设置目录等等。
3.给与Model匹配的View组件设置设置model。setModel()函数。
4.修改model中的数据就是等于修改了界面中数据,修改界面中的数据model中的数据也是修改的。
1.QFileSystemModel
QFileSystemModel类的基本功能
QFileSystemModel提供了一个可用于访问本机文件系统的数据模型。QFileSystemModel和视图组件QTreeView结合使用,可以用目录树的形式显示本机上的文件系统,如同Widnows的资源管理器一样。使用QFileSystemModel提供的接口函数,可以创建目录、删除目录、重命名目录,可以获得文件名称、目录名称、文件大小等参数,还可以获得文件的详细信息。
要通过QFileSystemModel获得本机的文件系统,需要用setRootPath()函数为QFileSystemModel设置一个根目录,例如:
QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());
静态函数QDir::currentPath()获取应用程序的当前路径。
用于获取磁盘文件目录的数据模型类还有一个QDirModel,QDirModel的功能与QFileSystemModel类似,也可以获取目录和文件,但是QFileSystemModel采用单独的线程获取目录文件结构,而QDirModel不使用单独的线程。使用单独的线程就不会阻碍主线程,所以推荐使用QFileSystemModel。
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个视图组件是如何显示这些数据的,则是其底层实现的问题了。
QStringListModel
这个模型是为了更好的显示出ListView的内容的,是跟ListView更好的匹配的。
1.QStringListModel功能概述
QStringListModel用于处理字符串列表的数据模型,它可以作为QListView的数据模型,在界面上显示和编辑字符串列表。
QStringListModel的setStringList()函数可以初始化数据模型的字符串列表的内容,stringList()函数返回数据模型内的字符串列表,在关联的ListView组件里编辑修改数据后,数据都会及时更新到数据模型内的字符串列表里。
QStringListModel提供编辑和修改字符串列表数据的函数,如insertRows()、removeRows()、setData()等,这些操作直接影响数据模型内部的字符串列表,并且修改后的数据会自动在关联的ListView组件里刷新显示。
2.QStringListModel的使用
1.Model/View结构对象和组件初始化
窗口是从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上双击一行进入编辑状态,修改一个项的文字内容后,这部分内容也保存到数据模型里了。
那么,数据模型内部应该保存有最新的数据内容,对于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里总是完全相同的,说明数据模型的数据与界面上显示的内容是同步的。
QStandardItemModel
这个模型是与表格中的数据是一个匹配的模型来的。
1.功能概述
QStandardItemModel是标准的以项数据(item data)为基础的标准数据模型类,通常与QTableView组合成Model/View结构,实现通用的二维数据的管理功能。
本节介绍QStandardItemModel的使用,主要用到以下3个类。
QStandardItemModel:基于项数据的标准数据模型,可以处理二维数据。维护一个二维的项数据数组,每个项是一个QStandardItem类的变量,用于存储项的数据、字体格式、对齐方式等。
QTableView:二维数据表视图组件,有多个行和多个列,每个基本显示单元是一个单元格,通过setModel()函数设置一个QStandardItemModel类的数据模型之后,一个单元格显示QStandardItemModel数据模型中的一个项。
QItemSelectionModel:一个用于跟踪视图组件的单元格选择状态的类,当在QTableView选择某个单元格,或多个单元格时,通过QItemSelectionModel可以获得选中的单元格的模型索引,为单元格的选择操作提供方便。
这几个类之间的关系是:QTableView是界面视图组件,其关联的数据模型是QStandardItem Model,关联的项选择模型是QItemSelectionModel,QStandardItemModel的数据管理的基本单元是QStandardItem。
打开一个纯文本文件,该文件是规则的二维数据文件,通过字符串处理获取表头和各行各列的数据,导入到一个QStandardItemModel数据模型。
编辑修改数据模型的数据,可以插入行、添加行、删除行,还可以在QTableView视图组件中直接修改单元格的数据内容。
可以设置数据模型中某个项的不同角色的数据,包括文字对齐方式、字体是否粗体等。
通过QItemSelectionModel获取视图组件上的当前单元格,以及选择单元格的范围,对选择的单元格进行操作。
将数据模型的数据内容显示到QPlainTextEdit组件里,显示数据模型的内容,检验视图组件上做的修改是否与数据模型同步。
将修改后的模型数据另存为一个文本文件。
2.界面设计与主窗口类定义
主窗口类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 ¤t, const QModelIndex &previous);
private:
Ui::MainWindow *ui;
};
这里定义了数据模型变量theModel,项数据选择模型变量theSelection。
定义的私有函数iniModelFromStringList()用于在打开文件时,从一个QStringList变量的内容创建数据模型。
自定义槽函数on_currentChanged()用于在TableView上选择单元格发生变化时,更新状态栏的信息显示,这个槽函数将会与项选择模型theSelection的currentChanged()信号关联。
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 ¤t, 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);
}
}`cpp
传递来的参数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上显示出来。
**添加行**
“添加行”操作是在数据表的最后添加一行,其实现代码如下:
```cpp
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::set TextAlignment()设置一个项的对齐方式即可。
“居中”和“居右”按钮的代码与此类似。
“粗体”按钮设置单元格的字体是否为粗体,在选择单元格时,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,tr("选择一个文件"),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<<str<<"\n"; //文件里需要加入换行符 \n
ui->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<<str<<"\n";
}
}
自定义代理
1.自定义代理的功能
简单点说,就是由于系统的View提供的组件不能很好的匹配到我们想要的数据内容,因此我们需要自己来设置自己想要的组件的内容。
QTableView组件为每个单元格提供的是缺省的代理编辑组件,就是一个QLineEdit组件。在编辑框里可以输入任何数据,所以比较通用。但是有些情况下,希望根据数据的类型限定使用不同的编辑组件,例如,第数据是整数,使用QSpinBox作为编辑组件更合适;数据是浮点数,使用QDoubleSpinBox更合适;布尔类型数据使用一个QComboBox,从一组列表文字中选择更合适。
要实现这些功能,就需要为TableView的某列或某个单元格设置自定义代理组件。为TableView增加自定义代理组件功能。
2.自定义代理类的基本设计要求
Qt中有关代理的几个类的层次结构如图所示。
图 实现代理功能的类的层次结构
QAbstractItemDelegate是所有代理类的抽象基类,QStyledItemDelegate是视图组件使用的缺省的代理类,QItemDelegate也是类似功能的类。QStyledItemDelegate与QItemDelegate的差别在于:QStyledItemDelegate可以使用当前的样式表设置来绘制组件,因此建议使用QStyledItem Delegate作为自定义代理组件的基类。
不管从QStyledItemDelegate还是QItemDelegate继承设计自定义代理组件,都必须实现如下的 4 个函数:
createEditor()函数创建用于编辑模型数据的widget组件,如一个QSpinBox组件,或一个QComboBox组件;
setEditorData()函数从数据模型获取数据,供widget组件进行编辑;
setModelData()将widget上的数据更新到数据模型;
updateEditorGeometry()用于给widget组件设置一个合适的大小。
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);
}
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()函数。