简介
模型/视图(model/view)结构是进行数据存储和界面展示的一种编程结构。此种方式将数据的存储与显示进行了解耦,视图组件显示模型中的数据,在视图组件里修改的数据会被自动保存到模型里。模型的数据来源可以是内存中的字符串列表或二维表格型数据,也可以是数据库中的数据表,一种模型可以用不同的视图组件来显示数据,所以模型/视图结构是一种高效、灵活的编程结构。
• 源数据(data)是原始数据,如数据库的一个数据表或 SQL 查询结果、内存中的一个字符串列表或磁盘文件系统结构等。
• 视图(view)也称为视图组件,是界面组件,视图从模型获得数据然后将其显示在界面上。 Qt 提供一些常用的视图组件,如 QListView、QTreeView 和 QTableView 等。
• 模型(model)也称为数据模型,与源数据通信,并为视图组件提供数据接口。它从源数 据提取需要的数据,用于视图组件进行显示和编辑。Qt 中有一些预定义的模型类,如 QStringListModel 是字符串列表的模型类,QSqlTableModel 是数据库中数据表的模型类。
• 代理(delegate)在视图与模型之间交互操作时提供的临时编辑器。模型向视图提供数据是单向的,一般仅用于显示。当需要在视图上编辑数据时,代理会为编辑数据提供一个编辑器,这个编辑器获取模型的数据、接受用户编辑的数据后又将其提交给模型。
模型结构有三种类型:列表模式、表格模式和树状模式,本文只探讨前两种用得较多的模式。
模型的索引
通过模型能访问的每个项都有一 个模型索引,视图组件和代理都 通过模型索引来获取数据。模型的基本形式是用行和列定义的表格数据,但这并不意味着底层的数据是用二维数组存储 的,使用行和列只是为了组件之间交互方便。当模型为列表或表格结构时,使用行号、列号访问数据比较直观,所有项的父项就是顶层项。
例如对于上述表格模型中的 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()表示。
而当模型涉及到列表项时,情况比较复杂,父节点的表示需要注意
例如上述树状模型,节点 A 和节点 C 的父节点是顶层节点,但是,节点 B 的父节点是节点 A,特别需要注意节点B的访问情况:
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
QModelIndex indexB = model->index(1, 0, indexA);
项的角色
模型中每一个项都有角色,设置项的数据的函数 setData(),其函数原型定义如下:
bool QAbstractItemModel::setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole)
其中,index 是项的模型索引,value 是需要设置的数据,role 是设置数据的角色。
常见的一些角色和枚举值如下表
同样,在获取一个项的数据时也需要指定角色,以获取不同角色的数据。函数 data(),可返回一个项的不同角色的数据,其函数原型定义如下:
QVariant QAbstractItemModel::data(const QModelIndex &index, int role = Qt::DisplayRole)
通过为一个项的不同角色定义数据,可以告知视图组件和代理如何展示数据。
例如下图,项的 DisplayRole角色数据是显示的字符串,DecorationRole 角色数据是用于装饰显示的元素(如图标),ToolTipRole 角色数据是就地显示的提示信息。
关联数据模型和选择模型
视图组件需要设置关联的模型才能构成完整的模型/视图结构。相关函数定义如下:
void setModel(QAbstractItemModel *model) //设置数据模型
QAbstractItemModel *model() //返回关联的数据模型对象指针
不同的视图组件使用不同类型的模型,QListView 组件一般用 QStringListModel 对象作为数据 模型,用于编辑字符串列表;QTableView 一般用 QStandardItemModel 对象作为数据模型,用于编辑表格数据。视图组件还可以设置选择模型,在界面上选择的项发生变化时,通过选择模型可以获取所有被选择项的模型索引。
相关函数定义如下:
void setSelectionModel(QItemSelectionModel *selectionModel) //设置选择模型
QItemSelectionModel *selectionModel() //返回关联的选择模型对象指针
常用接口函数
QAbstractItemView 定义了很多接口函数,下面是常用的几个。
QModelIndex currentIndex() //返回当前项的模型索引,例如当前单元格的模型索引
void setCurrentIndex(const QModelIndex &index) //设置模型索引为 index 的项为当前项
void selectAll() //选择视图中的所有项,例如选择 QTableView 组件中的所有单元格
void clearSelection() //清除所有选择
如果设置为单选,视图组件上就只有一个当前项,函数 currentIndex()返回当前项的模型索引,
通过模型索引就可以从模型中获取项的数据。
常用信号
QAbstractItemView 定义了几个信号,常用的几个信号定义如下,信号触发条件见注释。
void clicked(const QModelIndex &index) //点击某个项时
void doubleClicked(const QModelIndex &index) //双击某个项时
void entered(const QModelIndex &index) //鼠标移动到某个项上时
void pressed(const QModelIndex &index) //鼠标左键或右键被按下时
QStringListModel 和 QListView
QStringListModel 内部存储了一个字符串列表,这个字符串列表的内容自动显示在关联的 QListView 组件上,在 QListView 组件上双击某一行时,可以通过默认的代理组件(QLineEdit 组件)修改这一行字符串的内容,修改后的这行字符串自动保存到数据模型的字符串列表里。
在字符串列表中添加或删除行是通过 QStringListModel 的接口函数实现的,QListView 没有接 口函数用于修改数据,它只是用作数据显示和编辑的界面组件。通过 QStringListModel 的接口函 数修改字符串列表的内容后,关联的 QListView 组 件会自动更新显示内容。
例程详解
构造函数中创建了数据模型 m_model 并初始化其字符串列表数据,再将 m_model 设置 为界面上的 QListView 组件 listView 的数据模型,构造模型/视图结构。 程序运行后,界面上的 listView 里就会显示初始化的字符串列表的内容。
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);//初始化一个字符串列表的内容m_strList<<"北京"<<"上海"<<"天津"<<"河北"<<"山东"<<"四川"<<"重庆"<<"广东"<<"河南";m_model= new QStringListModel(this); //创建数据模型m_model->setStringList(m_strList); //为模型设置StringList,会导入StringList的内容ui->listView->setModel(m_model); //为listView设置数据模型:将数据模型与界面组件绑定ui->chkEditable->setChecked(true);ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked);}
数据模型的操作
可以对数据模型进行操作,例如添加项、删除项、 移动项等,相关操作会自动更新到界面
void MainWindow::on_btnIniList_clicked()
{ //恢复列表m_model->setStringList(m_strList); //重新载入到模型,界面会自动发生变化
}void MainWindow::on_btnListAppend_clicked()
{ //添加项m_model->insertRow(m_model->rowCount()); //在尾部插入一个数据项QModelIndex index=m_model->index(m_model->rowCount()-1,0, QModelIndex()); //获取刚插入的项的模型索引m_model->setData(index,"new item",Qt::DisplayRole); //设置显示文字,界面会自动发生变化ui->listView->setCurrentIndex(index); //将光标定位到新加的项
}void MainWindow::on_btnListInsert_clicked()
{//插入项QModelIndex index=ui->listView->currentIndex(); //当前项的模型索引m_model->insertRow(index.row()); //在当前项的前面插入一项m_model->setData(index,"inserted item",Qt::DisplayRole); //设置显示文字
// theModel->setData(index,Qt::AlignRight,Qt::TextAlignmentRole); //设置对齐方式,不起作用ui->listView->setCurrentIndex(index); //设置当前项
}void MainWindow::on_btnListDelete_clicked()
{//删除当前项QModelIndex index=ui->listView->currentIndex(); //获取当前项的模型索引m_model->removeRow(index.row()); //删除当前项
}void MainWindow::on_btnListClear_clicked()
{//清除列表//删除row=0为起始,长度为rowCount()的项,在这里即所有项m_model->removeRows(0,m_model->rowCount()); //清除数据模型的所有项
}void MainWindow::on_btnListSort_clicked(bool checked)
{//排序if (checked) //同样通过操作model即可自动更新到界面m_model->sort(0,Qt::AscendingOrder); //升序elsem_model->sort(0,Qt::DescendingOrder); //降序
}void MainWindow::on_btnListMoveUp_clicked()
{//上移int curRow=ui->listView->currentIndex().row(); //当前行号QModelIndex index=QModelIndex();m_model->moveRow(index,curRow,index,curRow-1);
}void MainWindow::on_btnListMoveDown_clicked()
{//下移int curRow=ui->listView->currentIndex().row(); //当前行号QModelIndex index=QModelIndex();m_model->moveRow(index,curRow,index,curRow+2);
}
对数据的操作都是通过数据模型的接口函数实现的。在数据模型 m_model 中添加或删除项后,界面组件 listView 中会立刻自动将其显示出来。
在对数据模型进行插入、添加、删除项操作后,内容会立即在 listView 上显示出来,这是数 据模型与视图组件之间信号与槽的作用的结果,当数据模型的内容发生改变时,通知视图组件更 新显示。在 listView 上双击一行进入编辑状态,修改一行的文字后,修改的文字也会保存到数据 模型里。 数据模型内保存着最新的数据内容,对 QStringListModel 模型来说,通过函数stringList()可以 得到其最新的数据副本。
QStandardItemModel 和 QTableView
QStandardItemModel 是以项为基本数据单元的模型类,每个项是一个 QStandardItem 对象。 项可以存储各种角色的数据,如文字、字体、对齐方式、图标、复选状态等。QStandardItemModel模型可以存储列表、表格、树3种模型数据。如果以多行多列的二维数组形式存储项,就是表格模型;如果表格模型只有一列,就是列表模型;如果在存储项时为项指定父项,就可以构成树状模型。
QStandardItemModel 数据模型中的每个项是一个 QStandardItem 对象。QStandardItem 存储了一 个项的各种特性参数,还可以存储用户自定义数据。一个项可以添加子项,子项也是 QStandardItem 类型的对象,所以,QStandardItem 也可以作为树状模型的项。
相关函数
void setRowCount(int rows) //设置数据模型的行数
void setColumnCount(int columns) //设置数据模型的列数void setItem(int row, int column, QStandardItem *item) //用于表格模型
void setItem(int row, QStandardItem *item) //用于列表模型QStandardItem *item(int row, int column = 0) //根据行号和列号返回项
QStandardItem *itemFromIndex(const QModelIndex &index) //根据模型索引返回项QModelIndex indexFromItem(const QStandardItem *item)void appendRow(const QList<QStandardItem *> &items) //用于表格模型
void appendRow(QStandardItem *item) //用于列表模型void appendColumn(const QList<QStandardItem *> &items) //在表格模型中添加列void insertRow(int row, const QList<QStandardItem *> &items) //用于表格模型
void insertRow(int row, QStandardItem *item) //用于列表模型bool insertRow(int row, const QModelIndex &parent = QModelIndex()) //用于树状模型void insertColumn(int column, const QList<QStandardItem *> &items) //用于表格模型
bool insertColumn(int column, const QModelIndex &parent = QModelIndex()) //用于树状模型QList<QStandardItem *> takeRow(int row) //移除一行,适用于表格模型
QList<QStandardItem *> takeColumn(int column) //移除一列,适用于表格模型QStandardItem *takeItem(int row, int column = 0) //移除一个项,适用于列表模型
QStandardItemModel 新定义了一个信号 itemChanged(),在任何一个项的数据发生变化时,此
信号就会被发射。信号函数定义如下,其中的参数 item 是数据发生了变化的项。
void itemChanged(QStandardItem *item)
例程详解
构造函数中创建数据模型和选择模型,并设置关联的界面组件(tableView)
MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this);m_model = new QStandardItemModel(2,FixedColumnCount,this); //创建数据模型m_selection = new QItemSelectionModel(m_model,this); //创建选择模型//选择当前单元格变化时的信号与槽connect(m_selection,&QItemSelectionModel::currentChanged,this,&MainWindow::do_currentChanged);// connect(theSelection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),// this,SLOT(do_currentChanged(QModelIndex,QModelIndex)));//为tableView设置数据模型ui->tableView->setModel(m_model); //设置数据模型ui->tableView->setSelectionModel(m_selection); //设置选择模型ui->tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);ui->tableView->setSelectionBehavior(QAbstractItemView::SelectItems);setCentralWidget(ui->splitter);//创建状态栏组件labCurFile = new QLabel("当前文件:",this);labCurFile->setMinimumWidth(200);labCellPos = new QLabel("当前单元格:",this);labCellPos->setMinimumWidth(180);labCellPos->setAlignment(Qt::AlignHCenter);labCellText = new QLabel("单元格内容:",this);labCellText->setMinimumWidth(150);ui->statusBar->addWidget(labCurFile);ui->statusBar->addWidget(labCellPos);ui->statusBar->addWidget(labCellText);
}void MainWindow::do_currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
{ //选择单元格变化时的响应Q_UNUSED(previous);if (current.isValid()){labCellPos->setText(QString::asprintf("当前单元格:%d行,%d列",current.row(),current.column())); //显示模型索引的行和列号QStandardItem *aItem=m_model->itemFromIndex(current); //从模型索引获得ItemlabCellText->setText("单元格内容:"+aItem->text()); //显示item的文字QFont font=aItem->font();ui->actFontBold->setChecked(font.bold());// 为了简化难度,没有设置ActionGroup,不能互斥选择
// Qt::Alignment align=aItem->textAlignment();
// ui->actAlignLeft->setChecked(align == Qt::AlignLeft);
// ui->actAlignCenter->setChecked(align == Qt::AlignHCenter);
// ui->actAlignRight->setChecked(align == Qt::AlignRight);}
}
读取ttxt文件内容并初始化Model,界面组件显示会自动发生变化
void MainWindow::iniModelData(QStringList &aFileContent)
{int rowCnt=aFileContent.size(); //文本行数,第1行是标题m_model->setRowCount(rowCnt-1); //实际数据行数//设置表头QString header=aFileContent.at(0); //第1行是表头QStringList headerList=header.split(QRegularExpression("\\s+"),Qt::SkipEmptyParts);m_model->setHorizontalHeaderLabels(headerList); //设置表头文字//设置表格数据int j;QStandardItem *aItem;for (int i=0;i<rowCnt-1;i++){QString aLineText=aFileContent.at(i); //获取 数据区 的一行//一个或多个空格、TAB等分隔符隔开的字符串, 分解为一个StringListQStringList tmpList=aLineText.split(QRegularExpression("\\s+"),Qt::SkipEmptyParts);for (j=0;j<FixedColumnCount-1;j++){ //不包含最后一列aItem=new QStandardItem(tmpList.at(j)); //创建itemm_model->setItem(i,j,aItem); //设置Item}aItem=new QStandardItem(headerList.at(j)); //最后一列是CheckableaItem->setCheckable(true); //设置为CheckableaItem->setBackground(QBrush(Qt::yellow));if (tmpList.at(j)=="0")aItem->setCheckState(Qt::Unchecked); //根据数据设置check状态elseaItem->setCheckState(Qt::Checked);m_model->setItem(i,j,aItem); //设置Item}
}
数据修改-添加-插入-删除
void MainWindow::on_actAppend_triggered()
{ //在表格最后添加行QList<QStandardItem*> aItemList; //列表QStandardItem *aItem;for(int i=0;i<FixedColumnCount-1;i++) //不包含最后1列{aItem=new QStandardItem("0"); //创建ItemaItemList<<aItem; //添加到列表}//获取最后一列的表头文字QString str=m_model->headerData(m_model->columnCount()-1,Qt::Horizontal,Qt::DisplayRole).toString();aItem=new QStandardItem(str); //创建 "测井取样"ItemaItem->setCheckable(true);aItemList<<aItem; //添加到列表m_model->insertRow(m_model->rowCount(),aItemList); //插入一行QModelIndex curIndex=m_model->index(m_model->rowCount()-1,0); //创建最后一行的ModelIndexm_selection->clearSelection(); //清空选择项m_selection->setCurrentIndex(curIndex,QItemSelectionModel::Select); //设置刚插入的行为当前选择行
}void MainWindow::on_actInsert_triggered()
{//插入行QList<QStandardItem*> aItemList; //QStandardItem的列表类QStandardItem *aItem;for(int i=0;i<FixedColumnCount-1;i++) //创建前5列{aItem=new QStandardItem("0"); //新建一个QStandardItemaItemList<<aItem;//添加到列表类}
// aItem=new QStandardItem("优"); //新建一个QStandardItem
// aItemList<<aItem;//添加到列表类QString str; //获取表头文字 //创建最后一列str=m_model->headerData(m_model->columnCount()-1,Qt::Horizontal,Qt::DisplayRole).toString();aItem=new QStandardItem(str); //创建ItemaItem->setCheckable(true);//设置为可使用CheckBoxaItemList<<aItem;//添加到列表类QModelIndex curIndex=m_selection->currentIndex(); //获取当前选中项的模型索引m_model->insertRow(curIndex.row(),aItemList); //在当前行的前面插入一行m_selection->clearSelection();//清除已有选择m_selection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}void MainWindow::on_actDelete_triggered()
{ //删除行QModelIndex curIndex=m_selection->currentIndex();//获取当前选择单元格的模型索引if (curIndex.row()==m_model->rowCount()-1)//最后一行m_model->removeRow(curIndex.row()); //删除最后一行else{m_model->removeRow(curIndex.row());//删除一行,并重新设置当前选择行m_selection->setCurrentIndex(curIndex,QItemSelectionModel::Select);}
}
遍历数据模型
void MainWindow::on_actModelData_triggered()
{//模型数据导出到PlainTextEdit显示ui->plainTextEdit->clear(); //清空QStandardItem *aItem;QString str;
// int i,j;//获取表头文字for (int i=0;i<m_model->columnCount();i++){aItem=m_model->horizontalHeaderItem(i); //获取表头的一个项数据str=str+aItem->text()+"\t"; //用TAB间隔文字}ui->plainTextEdit->appendPlainText(str); //添加为文本框的一行//获取数据区的每行for (int i=0;i<m_model->rowCount();i++){str="";for(int j=0; j<m_model->columnCount()-1;j++){aItem=m_model->item(i,j);str=str+aItem->text()+QString::asprintf("\t"); //以 TAB分隔}aItem=m_model->item(i,FixedColumnCount-1); //最后一行是逻辑型if (aItem->checkState()==Qt::Checked)str=str+"1";elsestr=str+"0";ui->plainTextEdit->appendPlainText(str);}
}