Qt的Model/View结构

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个参数完全可以定义一个项的位置,从而存取项的数据。

..\18-0113 图\0504.tif
图 数据模型的几种表现形式(来自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定义了鼠标提示信息。不同的视图组件对各种角色数据的解释和显示可能不一样,也可能忽略某些角色的数据。

图片 134
图 不同角色数据的表现形式(来自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 &current, 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 &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);
}
}`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中有关代理的几个类的层次结构如图所示。

..\18-0113 图\0512.tif图 实现代理功能的类的层次结构

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()函数。

猜你喜欢

转载自blog.csdn.net/simple_core/article/details/124633406