QT-模型视图结构
模型视图结构
一、模型/视图结构概述
模型/视图结构一种将数据存储和界面展示分离的编程方法。模型用于存储数据,视图组件用来显示模型中的数据。
在Qt中,模型-视图架构(Model-View Architecture)被广泛应用于数据驱动的用户界面设计中。与传统的MVC(Model-View-Controller)和MVVM(Model-View-ViewModel)模式类似,Qt的模型-视图架构也强调数据和表示的分离。Qt中的模型/视图架构主要涉及三个核心概念:模型(Model)、视图(View)和委托(Delegate)。
1、模型(Model)
模型是数据的容器。定义了数据的结构和逻辑,以及数据的访问和更新规则。
常见的模型包括QStandardItemModel
(用于通用的数据存储)、QSqlTableModel
(用于SQL数据库)和QFileSystemModel
(用于文件系统)等。
模型的数据来源可以是内存中的字符串列表,也可以是来自于数据库表中的数据。
同一模型可以采用用不同的视图组件来展示数据,所以模型/视图结构是一种高效灵活的编程结构。
2、视图(View)
视图是用户界面的一部分,用于展示模型中的数据。
在Qt中,常见的视图组件有QTableView
、QListView
、QTreeView
或QAbstractItemView
的子类,它们负责从模型中获取数据,并以适当的方式显示出来。视图通过信号和槽机制与模型通信,可以监听模型的数据变化,并相应地更新UI。
3、委托(Delegate)
委托在模型-视图架构中起到中介的作用,它决定了视图中数据项的外观和行为。
委托可以自定义数据项的绘制方式和编辑行为。比如为视图与模型之间的交互提供临时的编辑器,在QTableView组件上双击一个单元格来编辑数据时,单元格里就会出现一个QLineEdit组件,这个编辑框就是代理提供的临时编辑器,修改后会被自动保存在模型里。
模型、视图和代理使用信号和槽进行通信。当数据发生变化时,模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示操作信息;在编辑数据时,代理会发射信号告知模型和视图组件编辑器的状态。
- Model主要负责管理数据
- View主要用来显示数据
- Delegate主要负责控制视图中每个项的编辑和显示行为
二、模型类
1、模型类的继承关系
模型(Model)是用来给视图提供数据的,也叫数据模型。模型的数据来源可以是类、文件、数据库等。Qt中几个主要的模型类,继承关系如下:
模型类 | 功能 |
---|---|
QFileSystemModel | 用于表示计算机文件系统的模型类 |
QStringListModel | 用于表示字符串列表数据的模型类 |
QStandardItemModel | 标准的基于项的模型类,每个项是一个QStandardItem对象 |
QSqlQueryModel | 用于表示数据库SQL查询结果的模型类 |
QSqlTableModel | 用于表示数据库的一个数据表的模型类 |
2、模型的基本结构
QAbstractItemModel是所有模型类的基类,它的子类都是以表格的层次结构展示数据;视图组件按照这种规则来存取模型中的数据,以不同的形式展示。下图是模型的3种展示形式:
数据模型中存储数据的基本单元都是项(item),每个项有一个行号、一个列号,一个父项。3个模型都有一个隐藏的根项(root item)。
- 列表模型的存储结构就是一列。
- 表格模型的存储结构是规则的二维数组。
- 树状模型的项可以有子项。
三、视图组件
视图组件负责展示数据,它从模型获取数据并呈现给用户。当模型中的数据发生变化时,视图会自动更新显示。
Qt提供的视图组件主要有以下:
视图组件 | 功能 |
---|---|
QListView | 用于显示单列的列表数据,适用于一维数据的操作。 |
QTreeView | 用于显示树状结构数据,适用于树状结构数据的操作。 |
QTableView | 用于显示表格数据,适用于二维表格数据的操作。 |
QColumnView | 用多个OListView显示树状结构数据,树状结构的一层用一个QListView显示。 |
Undo View | 撤销命令视图 |
四、列表模型/视图
列表模型的存储结构就是一列
QStringListModel是字符串列表数据的模型类,通常与QListView组件搭配,组成模型/视图结构适合处理字符串列表数据。
我们使用QStringListModel和QListView来实现列表模型,如图所示。
ListWidget::ListWidget(QWidget *parent) // 构造函数,parent 表示父窗口指针: QWidget(parent) // 调用 QWidget 的构造函数,设置父对象, ui(new Ui::ListWidget) // 初始化 UI 指针
{ui->setupUi(this); // 初始化界面(加载 .ui 文件里的控件)QStringList list = {"北京","上海","成都"}; // 创建一个字符串列表,包含 3 个城市名QStringListModel *model = new QStringListModel(this); // 创建字符串列表模型,父对象是当前窗口model->setStringList(list); // 把字符串列表设置到模型中ui->listView->setModel(model); // 把模型绑定到界面上的 listView 控件,显示数据
}ListWidget::~ListWidget() // 析构函数
{delete ui; // 释放 UI 内存,防止内存泄漏
}
Qt 模型/视图(Model/View)架构就是为了解耦数据和显示,让复杂数据也能灵活展示。
1. 直接把数据写到视图里
比如 QListWidget
,可以直接用:
ui->listWidget->addItem("北京");
ui->listWidget->addItem("上海");
ui->listWidget->addItem("成都");
✅ 优点:
- 简单直观,几行代码就能完成。
- 小数据量、固定内容的情况下足够用。
❌ 缺点:
- 数据和视图强绑定,如果数据要更新(比如从数据库查询、网络接收),你得手动清空再重新添加。
- 如果多个视图要共享同一份数据,就没法直接复用。
2. 使用模型 + 视图(你现在用的方式)
QStringListModel *model = new QStringListModel(this);
model->setStringList(list);
ui->listView->setModel(model);
✅ 优点:
- 数据与视图分离:数据在
model
里,view
只管显示。 - 统一接口:数据可以来自
QStringList
、数据库 (QSqlTableModel
)、自定义类等,换源不用改view
代码。 - 支持多个视图共享同一份数据(比如一个列表和一个表格同时显示同一模型里的数据)。
- 支持大数据/懒加载,比如上万条记录,模型可以按需加载,节省内存。
❌ 缺点:
- 初学时显得繁琐。
- 小项目看起来“多此一举”。
3. 总结
- 小项目/数据固定 → 用
QListWidget
直接加 item 就行,简单方便。 - 大项目/数据来自数据库/网络 → 用
Model + View
更合适,扩展性强。
五、表格模型/视图
QStandardItemModel模型类,主要用于处理层次化的数据结构,能够存储和管理复杂的数据集合。
QStandardItemModel
可以用于多种视图组件,如QTableView
、QTreeView
、QListView
等,以适应不同的用户界面需求。
这里我们使用QStandardItemModel和QTableView来实现如图所示:
#include "tablewidget.h"
#include "ui_tablewidget.h"
#include <QStandardItemModel>TableWidget::TableWidget(QWidget *parent): QWidget(parent), ui(new Ui::TableWidget)
{ui->setupUi(this);// 定义二维数组,存放表格数据(姓名、部门、薪水、奖金)QVector<QVector<QString>> arr = {{"张三","研发部","10000","2000"},{"李四","市场部","12000","1000"},{"王五","研发部","12000","2000"}};// 创建一个标准模型,用来存放数据(行列数据模型)QStandardItemModel *model = new QStandardItemModel(this);// 定义表头标题QStringList headers = {"姓名", "部门", "薪水", "奖金"};// 设置水平表头(即列标题)model->setHorizontalHeaderLabels(headers);// 遍历二维数组,把数据逐行逐列填入模型for (int row = 0; row < 3; row++) // 遍历行{for (int col = 0; col < 4; col++) // 遍历列{// 每个表格单元格是一个 QStandardItem 对象QStandardItem *item = new QStandardItem(arr[row][col]);// 把 item 放到模型的 (row, col) 位置model->setItem(row, col, item);}}// 把模型绑定到视图(tableView 负责显示,model 负责存数据)ui->tableView->setModel(model);
}TableWidget::~TableWidget()
{delete ui; // 析构时释放 UI 资源
}
六、树状模型/视图
QFileSystemModel用于表示本地文件系统的目录和文件结构。
QFileSystemModel为本机的文件系统提供一个模型,结合使用QFileSystemModel和QTreeView可以以目录树的形式显示本机的文件系统。
这里我们使用QFileSystemModel和QTreeView实现树状模型,如图所示:
#include "treewidget.h"
#include "ui_treewidget.h"
#include <QFileSystemModel>
#include <QDebug>
#include <QTreeView>TreeWidget::TreeWidget(QWidget *parent): QWidget(parent), ui(new Ui::TreeWidget)
{ui->setupUi(this);// 创建一个文件系统模型 QFileSystemModel// 它会把磁盘上的文件夹和文件组织成树状结构,供 QTreeView 显示QFileSystemModel *model = new QFileSystemModel(this);// 设置模型的根路径为当前工作目录// 这样模型会从当前目录开始加载文件和文件夹信息model->setRootPath(QDir::currentPath());// 把模型绑定到 QTreeView(treeView 是界面里的树形控件)ui->treeView->setModel(model);// 设置 QTreeView 的根索引为当前工作目录// 这样树视图就不会从磁盘根目录开始,而是直接展示当前目录的内容ui->treeView->setRootIndex(model->index(QDir::currentPath()));
}TreeWidget::~TreeWidget()
{delete ui; // 析构时释放 UI 资源
}
七、模型视图结构的相关概念
1、模型索引
数据模型中引入了模型索引(model index)的概念。通过模型能访问的每一个项都有一个模型索引。
- 模型索引是对模型内部数据结构中单个数据项的唯一标识符,它包含了行号、列号以及可能的父索引等信息,用于精确地定位模型中的某个数据元素。
- QModelIndex 表示模型索引的类。模型索引提供数据存取的一个临时指针,用于通过数据模型提取或修改数据。
#include "listwidget.h"
#include "ui_listwidget.h"
#include <QStringListModel>ListWidget::ListWidget(QWidget *parent): QWidget(parent), ui(new Ui::ListWidget)
{ui->setupUi(this);// 定义一个 QStringList(字符串列表),作为初始数据源QStringList list;list << "北京" << "上海" << "成都";// 创建一个 QStringListModel(字符串列表模型)// 它会把 QStringList 中的内容映射为可显示的数据项QStringListModel *model = new QStringListModel(this);// 将字符串列表数据加载到模型中model->setStringList(list);// 将模型绑定到界面中的 QListView(listView 是 UI 里的控件)// 这样 listView 就会显示 model 的数据ui->listView->setModel(model);// 获取模型中第 0 行,第 0 列的索引(索引从 0 开始)QModelIndex index = model->index(0, 0);// 修改指定索引的数据// Qt::DisplayRole 表示用于显示的数据,这里把 "北京" 改成了 "天津"model->setData(index, "天津", Qt::DisplayRole);
}ListWidget::~ListWidget()
{delete ui; // 析构时释放 UI 资源
}
2、行号和列号
数据模型的基本形式是用行和列定义的表格数据,但这并不意味着底层的数据就是是用二维数组存储的,使用行和列只是为了组件之间交互方便的一种规定。通过模型索引的行号和列号就可以存取数据。
要获得一个模型索引,可以提供 3 个参数:行号、列号、父项的模型索引。例如,对于如图中的表格数据模型中的 3 个数据项 A、B、C,
获取其模型索引的代码是:
QModelIndex()
空索引表示模型的根项或没有父项的顶级项。
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());
在创建模型索引的函数中需要传递行号、列号和父项的模型索引。对于列表和表格模式的数据模型,顶层节点用 QModelIndex() 表示。
3、父项
当数据模型是列表或表格时,使用行号、列号存储数据比较直观,所有数据项的父项就是根项;当数据模型是树状结构时,情况比较复杂(树状结构中,项一般习惯于称为节点),一个节点可以有父节点,也可以是其他节点的父节点,在构造树状结构数据项的模型索引时,必须指定正确的行号、列号和父节点。
对于上图中的树状数据模型,节点 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);
4、项的角色
通过为一个项指定不同的角色,可以告知视图组件和代理如何展示数据。
在为模型的一个项设置数据时,可以为项设置不同角色的数据。QAbstractItemModel类定义了设置项的数据函数,原型如下:
bool QAbstractItemModel::setData(const QModelIndex &index, const QVariant &value, int role= Qt::EditRole)
QStandardItemModel model; // 创建模型
QModelIndex index = model.index(0, 0); // 获取第0行第0列的索引(假设已有数据)// 修改显示文本(默认 role 是 EditRole)
model.setData(index, "新文本", Qt::EditRole);// 设置提示信息(鼠标悬停时显示)
model.setData(index, "这是提示信息", Qt::ToolTipRole);// 设置前景色(文字颜色为红色)
model.setData(index, QColor("red"), Qt::ForegroundRole);// 设置背景色(背景为黄色)
model.setData(index, QColor("yellow"), Qt::BackgroundRole);// 设置勾选框为已选中
model.setData(index, Qt::Checked, Qt::CheckStateRole);
其中,index是项的模型索引,value是要设置的数据,role是设置数据的角色。角色参数role用枚举类型Qt::ItemDataRole的枚举值表示:
枚举值 | 角色数据类型 | 含义 |
---|---|---|
Qt::DisplayRole | QString | 界面上显示的字符串,例如单元格显示的文字 |
Qt::DecorationRole | QIcon、QColor | 在界面上起装饰作用的数据,如图标 |
Qt::EditRole | QString | 界面上适合在编辑器中显示的数据,一般是文字 |
Qt::ToolTipRole | QString | 项的toolTip字符串 |
Qt::FontRole | QFont | 项的字体,如单元格内文字的字体 |
Qt::TextAlignmentRole | Qt::Alignment | 项的对齐方式,如单元格内文字的对齐方式 |
Qt::BackgroundRole | QBrush | 项的背景色,如单元格的背景色 |
Qt::ForegroundRole | QBrush | 项的前景色,如单元格的文字颜色 |
Qt::CheckStateRole | Qt::CheckState | 项的复选状态 |
在获取一个项的数据时也可以指定角色,以获取不同角色的数据。QAbstractItemModel定义了函数data(),可以返回一个项的不同角色的数据,原型:
QVariant QAbstractItemModel::data(const QModelIndex &index, int role = Qt::DisplayRole)
QStandardItemModel model; // 创建模型
model.setItem(0, 0, new QStandardItem("北京")); // 在第0行第0列放一个数据QModelIndex index = model.index(0, 0); // 获取第0行第0列的索引// 取出显示的文本(默认 role 是 DisplayRole)
QString text = model.data(index, Qt::DisplayRole).toString();
qDebug() << "显示文本:" << text;// 取出编辑角色的数据(其实等价于 DisplayRole)
QString editText = model.data(index, Qt::EditRole).toString();
qDebug() << "编辑文本:" << editText;// 如果之前设置过 ToolTipRole
QString tooltip = model.data(index, Qt::ToolTipRole).toString();
qDebug() << "提示信息:" << tooltip;
📌 总结:
setData()
→ 写入数据data()
→ 读取数据role
决定读/写哪种用途的数据(显示、编辑、提示、颜色、勾选状态等)。
模型索引和项角色使用案例:
#include "standardmainwindow.h"
#include "ui_standardmainwindow.h"
#include <QTreeView>
#include <QStandardItemModel>
#include <QDebug>StandardMainWindow::StandardMainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::StandardMainWindow)
{ui->setupUi(this);QTreeView *view = new QTreeView(this); // 创建一个树形视图控件view->resize(200, 300); // 设置视图大小QStandardItemModel *model = new QStandardItemModel(this); // 创建标准项模型,用于存储树形数据QStandardItem *root = model->invisibleRootItem(); // 获取不可见的根节点,所有顶级节点都挂在它下面QStandardItem *item0 = new QStandardItem; // 创建第一个子节点item0->setData("节点A", Qt::EditRole); // 设置显示文本(等价于 setText)item0->setData("提示信息", Qt::ToolTipRole); // 设置提示信息(鼠标悬停显示)root->appendRow(item0); // 把节点A挂到根节点下QStandardItem* item1 = new QStandardItem; // 创建第二个子节点item1->setData("节点B", Qt::EditRole); // 设置显示文本item1->setData(QIcon(":/image/1_2.png"), Qt::DecorationRole); // 设置图标root->appendRow(item1); // 把节点B挂到根节点下QStandardItem *item2 = new QStandardItem; // 创建第三个子节点item2->setData("节点C", Qt::EditRole); // 设置显示文本item2->setData(QColor("red"), Qt::BackgroundRole); // 设置背景色为红色item2->setCheckable(true); // 设置为可勾选item2->setData(Qt::Checked, Qt::CheckStateRole); // 设置勾选状态为“已勾选”root->appendRow(item2); // 把节点C挂到根节点下view->setModel(model); // 将模型绑定到视图,数据就会显示出来// ========== 测试获取各个节点的数据 ==========QModelIndex index0 = model->index(0, 0, QModelIndex()); // 获取第 0 行节点(节点A)qDebug() << "节点A的文本:" << model->data(index0, Qt::EditRole).toString(); // 输出显示文本qDebug() << "节点A的提示文字:" << model->data(index0, Qt::ToolTipRole).toString(); // 输出提示信息QModelIndex index1 = model->index(1, 0, QModelIndex()); // 获取第 1 行节点(节点B)qDebug() << "节点B的文本:" << model->data(index1, Qt::EditRole).toString(); // 输出显示文本qDebug() << "节点B的装饰:" << model->data(index1, Qt::DecorationRole); // 输出装饰(图标)qDebug() << "节点B的字体:" << model->data(index1, Qt::FontRole).toString(); // 输出字体(未设置,默认空)QModelIndex index2 = model->index(2, 0, QModelIndex()); // 获取第 2 行节点(节点C)qDebug() << "节点C的文本:" << model->data(index2, Qt::EditRole).toString(); // 输出显示文本qDebug() << "节点C的背景色:" << model->data(index2, Qt::BackgroundRole); // 输出背景色qDebug() << "节点C的勾选状态:" << model->data(index2, Qt::CheckStateRole); // 输出勾选状态
}StandardMainWindow::~StandardMainWindow()
{delete ui; // 析构时释放 UI 资源
}
八、QAbstractItemModel类函数接口
QAbstractItemModel是所有模型类的直接或间接父类,定义了模型的通用接口函数。
例如:插入行、删除行、设置数据的函数。
1、行数和列数
函数rowCount
返回行数,columnCount()
返回列数,两个函数的原型定义如下:
int rowCount(const QModelIndex &parent = QModelIndex());
int columnCount(const QModelIndex &parent = QModelIndex());
这两个函数中都需要传递一个参数parent,这是父项的模型索引。
- 对于列表模型和表格模型,parent使用默认的参数QModelIndex()即可,得到的行数和列数就是模型的行数和列数。
- 对于树状模型,parent需要设置为父节点的模型索引,函数返回的是父节点下的节点的行数和列数。
2、插入或删除行
bool insertRow(int row, const QModelIndex &parent = QModelIndex())virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex())bool removeRow(int row, const QModelIndex &parent = QModelIndex())virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex())
参数parent是父项的模型索引。对于列表模型和表格模型,parent使用默认的参数QModelIndex()即可。对于树状模型,parent需要设置为父节点的模型索引。
在使用函数insertRow()时,如果参数row的值超过了模型的行数,新增的行就添加到模型的末尾。
3、插入或删除列
bool insertColumn(int column, const QModelIndex &parent = QModelIndex())virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex())
bool removeColumn(int column, const QModelIndex &parent = QModelIndex())virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex())
4、移动行或列
可以实现表格中的行或列的移动,以及目录树中的节点移动等。
参数一:源行的父项索引
参数二:源行的位置
参数三:目标行的父索引
参数四:目标位置的索引,这个索引指的是插入点,即新行将被放置在该索引之前的位置。
bool moveRow(const QModelIndex &sourceParent, int sourceRow, const QModelIndex &destinationParent, int destinationChild)
bool moveColumn(const QModelIndex &sourceParent, int sourceColumn, const QModelIndex &destinationParent, int destinationChild)
5、数据排序
函数sort()将数据按某一列排序,可以指定排序方式,默认是升序方式。
virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder)
6、设置和读取项的数据
函数setData()为一个项设置数据,函数data()返回一个项的数据,其函数原型定义如下:
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole)
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0
这里,我们以QStingListModel
和QListView
模型/视图组件来演示数据模型的接口函数:
#include "listwidget.h"
#include "ui_listwidget.h"
#include <QStringListModel>ListWidget::ListWidget(QWidget *parent): QWidget(parent), ui(new Ui::ListWidget)
{ui->setupUi(this);list << "北京" << "上海" << "成都"; // 初始化字符串列表model = new QStringListModel(this); // 创建字符串列表模型model->setStringList(list); // 把数据设置到模型ui->listView->setModel(model); // 视图绑定模型
}ListWidget::~ListWidget()
{delete ui;
}void ListWidget::on_btnInit_clicked()
{model->setStringList(list); // 恢复初始数据
}void ListWidget::on_btnClear_clicked()
{model->removeRows(0, model->rowCount()); // 清空所有行
}void ListWidget::on_btnAdd_clicked()
{model->insertRow(model->rowCount()); // 在最后插入一行QModelIndex index = model->index(model->rowCount()-1, 0); // 获取新行索引model->setData(index, "新建项"); // 设置新行的数据ui->listView->setCurrentIndex(index); // 选中新行
}void ListWidget::on_btnInsert_clicked()
{QModelIndex index = ui->listView->currentIndex(); // 获取当前选中项索引model->insertRow(index.row()); // 在当前行插入新行model->setData(index, "插入项", Qt::EditRole); // 设置新行内容ui->listView->setCurrentIndex(index); // 选中插入行
}void ListWidget::on_btnDelete_clicked()
{QModelIndex index = ui->listView->currentIndex(); // 获取当前选中项model->removeRow(index.row()); // 删除该行
}void ListWidget::on_btnUp_clicked()
{int currentRow = ui->listView->currentIndex().row(); // 获取当前行号QModelIndex index = QModelIndex(); // 父索引为空model->moveRow(index, currentRow, index, currentRow-1); // 向上移动一行
}void ListWidget::on_btnDown_clicked()
{int currentRow = ui->listView->currentIndex().row(); // 获取当前行号QModelIndex index = QModelIndex(); // 父索引为空model->moveRow(index, currentRow, index, currentRow+2); // 向下移动一行
}void ListWidget::on_btnSort_clicked(bool checked)
{static int sort = 0; // 排序模式标志if (sort){model->sort(0, Qt::AscendingOrder); // 升序sort = 0;}else{model->sort(0, Qt::DescendingOrder); // 降序sort = 1;}
}void ListWidget::on_checkBox_clicked(bool checked)
{if (checked) {ui->listView->setEditTriggers( // 开启编辑:双击或选中QAbstractItemView::DoubleClicked |QAbstractItemView::SelectedClicked);}else {ui->listView->setEditTriggers( // 禁止编辑QAbstractItemView::NoEditTriggers);}
}
从上述代码可以看出,对数据的操作都是通过数据模型的接口函数来实现的。在数据模型添加和删除项后,界面组件QListView中会立刻自动将其显示出来。