文章目录
- Model/View
- Model/View编程的优点
- 常见Model类和View类
- Model/View应用程序示例
- 只读的表格
- 修改文本外观
- 显示变化的数据
- 设置表格标头
- 可编辑视图示例
- 树结构视图示例
- 获取视图选中项
Model/View
Model/View编程的优点
Model/View编程介绍:https://doc.qt.io/qt-5/model-view-programming.html
表格(Table)、列表(List)和树(Tree )形控件(widget)是图形用户界面(GUI)中经常使用的组件。这些控件有两种不同的方式来访问其数据,即直接访问和通过Model/View的模式访问。
直接访问
一般的控件自身包含了用于存储数据的容器,可以直接访问数据:
这样使用起来很直观,但是有以下几个问题:
(1)数据冗余与同步问题:每个控件都维护自己的数据容器,多个控件可能会存储相同的数据,导致冗余。如果数据需要在多个控件间同步,开发者需要手动维护,容易导致数据不一致。
(2)难以复用和扩展:由于数据存储和视图绑定在一起,无法轻松替换数据源或调整数据结构。
不能灵活适配不同的数据来源,例如数据库、文件、远程 API 等。
(3)耦合度高:视图和数据紧密耦合,控件既要管理 UI,又要管理数据,违反了单一职责原则。难以单独测试数据逻辑或 UI 逻辑,不利于单元测试和维护。
(4)性能问题:当数据量较大时,控件自身存储数据可能导致高内存占用。需要加载完整数据集,而不是按需加载,影响性能。
Model/View
- Model-View将控件的数据与视图分离。控件不维护内部数据容器,它们通过标准化接口访问外部数据。在Model-View中,模型(Model)是数据的抽象表示,负责存储和管理数据。视图(View)是用于显示模型中的数据,并与用户进行交互的组件。视图通常会提供一个setModel()方法,允许开发者将视图与一个特定的模型实例相关联。一旦设置了模型,视图就可以通过该模型访问和显示数据。
相比于直接访问,Model/View有以下优点:
(1)数据和视图分离,低耦合:视图仅负责显示数据,数据逻辑由模型管理,符合MVC(Model-View-Controller)设计思想。修改数据源或替换模型不会影响视图,增强了代码的灵活性和可维护性。
(2)数据共享与同步更方便:多个视图可以共享同一个模型,避免数据冗余。数据变更时,模型可以通知所有关联视图(例如 Qt 的 QAbstractItemModel 提供 dataChanged() 信号),自动更新 UI。
(3)支持大规模数据处理:视图只会访问模型提供的接口,而模型可以实现懒加载(按需加载数据),提高性能。例如,视图可以根据需要请求数据,而不是一次性加载所有数据。
(4)提高代码复用性:相同的模型可以被不同的视图复用,例如列表视图、树状视图、表格视图等都可以使用同一个数据模型。适用于不同的 UI 组件,减少重复代码。
(5)更好的测试和维护性:由于视图和数据分离,可以单独测试数据模型,提高代码质量。UI 变更不会影响数据逻辑,降低了维护成本。
常见Model类和View类
QT5中常见的模型(Model)类包括:
- QAbstractItemModel:所有模型类的基类,定义了一些纯虚函数,需要子类来实现以提供自定义的数据存储和访问方式。
- QAbstractListModel:列表模型的抽象基类,适用于一维数据。
- QAbstractTableModel:表格模型的抽象基类,适用于二维数据。
- QStringListModel:用于处理字符串列表数据的数据模型类。
- QStandardItemModel:标准的基于数据项的数据模型类,每个数据项都可以是任何数据类型。
- QFileSystemModel:计算机上文件系统的数据模型类,提供了对本地文件系统的访问和操作。
- QSortFilterProxyModel:与其他数据模型结合,提供排序和过滤功能的数据类型模型类。
- QSqlTableModel:用于数据库的一个数据表的数据模型类。
- QSqlRelationalTableModel:用于关系类型数据表的数据模型。
QT5中常见的视图(View)类包括:
- QListView:用于显示单列的列表数据,适用于一维数据的操作。
- QTreeView:用于显示树状结构数据,适用于树状结构数据的操作。
- QTableView:用于显示表格状数据,适用于二维表格型数据的操作。
- QColumnView:用多个QListView显示树状层次结构,树状结构的一层用一个QListView显示。
注:一些标准控件类,如QListWidget、QTreeWidget和QTableWidget,它们是上述视图类的子类。
下面是部分的标准控件(item-based)和对应的Model/View 控件(Model-based)的效果示例:
列表List:QListWidget,QListView
表格:QTableWidget QTableView
树:QTreeWidget QTreeView
QColumnView:
特别地,QComboBox既可以作为标准的控件,也可以作为Model/View 控件:
对于那些操作单个值而不是数据集的控件(如QLineEdit、QCheckBox等),没有直接的Model/View对应项来分离数据和视图,因此我们需要一个适配器(Adapters )来将表单连接到数据源。例如,QDataWidgetMapper和QCompleter。
QT官网提供了各个类的使用示例:
https://doc.qt.io/qt-5/examples-itemviews.html
下面将以一些简单的例子展示Model/View用法。
Model/View应用程序示例
下面用七个简单的应用程序,展示模型/视图编程:
只读的表格
代码:[github]
我们从一个使用QTableView显示数据的应用程序开始。
// main.cpp
#include <QApplication>
#include <QTableView> // 引入的视图类,用于以表格形式显示数据。
#include "mymodel.h" // 引入自定义的模型类头文件,用于提供数据给视图。
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
// 创建QTableView对象tableView
QTableView tableView;
// 创建自定义模型对象myModel
MyModel myModel;
// 通过调用tableView的setModel方法,将自定义模型myModel设置为tableView的数据源。
// 这样,tableView就可以从myModel中获取数据并进行显示了。
tableView.setModel(&myModel);
// 调用tableView的show方法,显示表格视图。
tableView.show();
return a.exec();
}
tableView.setModel(&myModel);
将它的指针传递给 tableView
。tableView
会调用它所接收到的指针的方法来了解两件事:
- 应该显示多少行和列。
- 每个单元格应该打印什么内容。
模型需要一些代码来响应这些请求。假设需要展示一个表格数据,让我们以 QAbstractTableModel
作为基类来自定义模型:
// mymodel.h
#include <QAbstractTableModel>
// 声明MyModel类,它继承自QAbstractTableModel
class MyModel : public QAbstractTableModel {
Q_OBJECT
public:
explicit MyModel(QObject *parent = nullptr);
// 重写rowCount函数,返回模型中的行数
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
// 重写columnCount函数,返回模型中的列数
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
// 重写data函数,用于提供模型中特定单元格的数据
// index参数指定了单元格的位置(行和列),role参数指定了所需数据的类型(如显示文本、工具提示等)
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};
模型的源文件:
// mymodel.cpp
#include "mymodel.h"
MyModel::MyModel(QObject *parent)
: QAbstractTableModel(parent)
{
}
// 实现rowCount函数,返回模型中的行数
int MyModel::rowCount(const QModelIndex & /*parent*/) const
{
// 表格模型没有使用父/子结构,
// 所以我们可以忽略parent参数(它通常用于树形或层次结构模型)
// 在这个例子中,我们简单地返回2,表示模型有两行
return 2;
}
// 实现columnCount函数,返回模型中的列数
int MyModel::columnCount(const QModelIndex & /*parent*/) const
{
// 与rowCount同样地,忽略parent参数
// 简单地返回3,表示模型有三列
return 3;
}
// 实现data函数,用于提供模型中特定单元格的数据
QVariant MyModel::data(const QModelIndex &index, int role) const
{
// 我们只对Qt::DisplayRole感兴趣,这是用于在视图中显示的数据
if (role == Qt::DisplayRole) {
// 使用QString的arg函数来格式化字符串,
// QModelIndex是Qt中用于定位数据模型数据的一个类,
// 调用其方法,将行号和列号(都加1以符合人类阅读习惯,从1开始计数)插入到字符串中
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() + 1);
}
// 对于其他类型的role(如编辑、工具提示等),我们返回QVariant的默认值,
// 表示没有提供这些数据
return QVariant();
}
当视图需要知道单元格的文本内容时,它会调用 MyModel::data()
方法。行和列的信息通过参数 index
指定,并且role
被设置为 Qt::DisplayRole
。
在这个例子中,需要显示的数据是指定的。在实际应用中,MyModel
一般会有一个名为 MyData
的成员,它作为所有读写操作的目标。
修改文本外观
代码:[github]
除了控制视图显示的文本外,模型还可以控制文本的外观。只需要添加更多的role
条件,就可以以下结果:
每个格式化属性都将通过单独调用data()方法从模型中请求。role参数用于让模型知道正在请求哪个属性:
// mymodel.cpp
QVariant MyModel::data(const QModelIndex &index, int role) const {
int row = index.row(); // 从QModelIndex对象中获取当前行的索引
int col = index.column(); // 从QModelIndex对象中获取当前列的索引
switch (role) {
case Qt::DisplayRole: // 处理显示角色的数据
// 为特定单元格设置自定义显示文本
if (row == 0 && col == 1) return QString("<--left");
if (row == 1 && col == 1) return QString("right-->");
// 为其他单元格返回默认的行列信息文本
return QString("Row%1, Column%2")
.arg(row + 1)
.arg(col + 1);
case Qt::FontRole: // 处理字体角色的数据
// 仅为单元格(0,0)设置粗体字体
if (row == 0 && col == 0) {
QFont boldFont;
boldFont.setBold(true);
return boldFont;
}
break;
case Qt::BackgroundRole: // 处理背景角色的数据
// 仅为单元格(1,2)设置红色背景
if (row == 1 && col == 2)
return QBrush(Qt::red);
break;
case Qt::TextAlignmentRole: // 处理文本对齐角色的数据
// 仅为单元格(1,1)设置文本对齐方式(右对齐和垂直居中)
if (row == 1 && col == 1)
return int(Qt::AlignRight | Qt::AlignVCenter);
break;
case Qt::CheckStateRole: // 处理复选框状态角色的数据
// 在单元格(1,0)中添加一个已选中的复选框
if (row == 1 && col == 0)
return Qt::Checked;
break;
}
return QVariant();
}
显示变化的数据
代码:[github]
以上的例子展示了模型的被动性质。模型不知道它何时会被使用,也不知道需要哪些数据。它只是在视图每次请求数据时提供数据。那么,当模型的数据需要改变时会发生什么呢?视图如何意识到数据已经改变并需要重新读取呢?
简而言之,当模型的数据发生变化时,它必须通知视图。这通常是通过发出一个信号来完成的,该信号携带了关于哪些数据已经改变的信息。视图接收到这个信号后,就会知道它需要重新从模型中读取数据来更新其显示。
在一个表格的第一行第一列展示当前时间:
// mymodel.cpp
QVariant MyModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
int col = index.column();
if (role == Qt::DisplayRole && row == 0 && col == 0)
return QTime::currentTime().toString();
return QVariant();
}
下面使用定时器和信号槽机制来实现表格中时间的更新,每隔1000ms,更新一次。首先在头文件中声明定时器和槽函数。
// mymodel.h
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit MyModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private slots:
// 每隔1000ms,模型发出一个信号,告诉视图哪些单元格的数据已经改变。
void timerHit();
private:
QTimer *timer; // 计时器
};
在构造函数中,我们将定时器的间隔设置为1秒,并连接其超时信号到槽函数timerHit()
。
// mymodel.cpp
MyModel::MyModel(QObject *parent)
: QAbstractTableModel(parent)
, timer(new QTimer(this))
{
// 超时信号,时间间隔设置为1000ms(1s)
timer->setInterval(1000);
connect(timer, &QTimer::timeout , this, &MyModel::timerHit);
timer->start();
}
槽函数接收到超时信号后,进一步地发出dataChanged()
信号来请求视图再次读取左上角单元格对应的数据:
// mymodel.cpp
void MyModel::timerHit()
{
QModelIndex topLeft = createIndex(0,0);
//发出信号,使视图重新读取topLeft中已识别的数据
emit dataChanged(topLeft, topLeft, {Qt::DisplayRole});
}
我们通过值得注意的是,我们并没有明确地将dataChanged()
信号与视图相连接。当我们调用setModel()
方法时,这一连接是自动完成的。
设置表格标头
代码:[github]
表格的标头可以通过视图方法隐藏:
// main.cpp
#include <QApplication>
#include <QTableView>
#include "mymodel.h"
#include <QHeaderView>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QTableView tableView;
MyModel myModel;
tableView.setModel(&myModel);
tableView.horizontalHeader()->hide(); // 隐藏水平标头
tableView.verticalHeader()->hide(); // 隐藏垂直标头
tableView.show();
return a.exec();
}
另一方面,标头内容可以通过重写headerData()
方法实现修改:
//mymodel.h
#include <QAbstractTableModel>
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit MyModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; // 重写headerData()方法
};
//mymodel.cpp
QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
switch (section) {
case 0:
return QString("first");
case 1:
return QString("second");
case 2:
return QString("third");
}
}else if (role == Qt::DisplayRole && orientation == Qt::Vertical) {
switch (section) {
case 0:
return QString("first");
case 1:
return QString("second");
}
}
return QVariant();
}
可编辑视图示例
代码:[github]
在这个例子中,我们将构建一个应用程序,该程序将获取用户输入到表格单元格中的值,并自动填充到窗口标题。
为了方便地访问窗口标题,我们将QTableView放置在QMainWindow中。
// mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, tableView(new QTableView(this))
{
setCentralWidget(tableView);
auto *myModel = new MyModel(this);
tableView->setModel(myModel);
// 将模型所作的更改显示到窗口标题
connect(myModel, &MyModel::editCompleted,
this, &QWidget::setWindowTitle);
}
模型决定了是否提供编辑功能。我们只需要修改模型,重写虚函数setData()
和flags()
即可启用编辑功能。
// mymodel.h
#include <QAbstractTableModel>
#include <QString>
const int COLS= 3;
const int ROWS= 2;
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
MyModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
QString m_gridData[ROWS][COLS]; // 保存输入QTableView的文本
signals:
void editCompleted(const QString &); // 编辑完成的信号
};
我们使用二维数组QString m_gridData
来存储数据。这使得m_gridData
成为MyModel的核心。MyModel的其余部分则像一个包装器,将m_gridData
适配到QAbstractItemModel
接口
// mymodel.cpp
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role == Qt::EditRole) {
if (!checkIndex(index))
return false;
//将值从编辑器保存到成员m_gridData
m_gridData[index.row()][index.column()] = value.toString();
// 将各个单元格的值凭拼接成字符串,通过editCompleted信号发送给标题
QString result;
for (int row = 0; row < ROWS; row++) {
for (int col= 0; col < COLS; col++)
result += m_gridData[row][col] + ' ';
}
emit editCompleted(result);
return true;
}
return false;
}
每次用户编辑一个单元格时,都会调用setData()
方法。index
参数告诉我们哪个字段被编辑了,而value
参数提供了编辑过程的结果。
由于我们的单元格只包含文本,因此role
将始终被设置为Qt::EditRole。如果存在一个复选框,并且用户权限被设置为允许选择该复选框,那么当role被设置为Qt::CheckStateRole时,也会进行调用。
// mymodel.cpp
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}
通过flags()
方法可以调整单元格的各种属性。
- Qt::ItemIsSelectable:表示单元格是可以被选中的。这是模型/视图框架中的默认行为之一,通常不需要显式指定,因为QAbstractTableModel::flags(index)默认会包含这个标志
- Qt::ItemIsEditable:表示单元格是可以被编辑的。
- Qt::ItemIsEnabled:表示单元格是启用的,即它是可交互的。这同样是模型/视图框架中的默认行为之一,通常不需要显式指定。
树结构视图示例
代码:[github]
使用模型/视图的典型方法是包装特定数据,使其可用于视图类。然而,Qt也为常见的底层数据结构提供了预定义的模型。例如,下面我们将通过预定义的模型QStandardItemModel
,用树形结构展示数据:
QStandardItemModel
它是一个用于存储层次结构数据的容器,必须被填充QStandardItems
。
// mainwindow.h
#include <QMainWindow>
// Qt 的命名空间开始,这里使用了前向声明来减少头文件的依赖
QT_BEGIN_NAMESPACE
class QTreeView; // QTreeView 类的前向声明,用于在 MainWindow 中作为成员变量
class QStandardItemModel; // QStandardItemModel 类的前向声明,用于存储树形视图的数据
class QStandardItem; // QStandardItem 类的前向声明,通常用于构建 QStandardItemModel 的内容
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private:
// 辅助函数,用于准备一行数据,并返回一个包含三个 QStandardItem* 的列表
// 这些 QStandardItem 分别对应于行中的第一、第二和第三个数据项
QList<QStandardItem*> prepareRow(const QString &first,
const QString &second,
const QString &third) const;
// 成员变量
QTreeView *treeView; // 指向 QTreeView 的指针,用于显示树形结构的数据
QStandardItemModel *standardModel; // 指向 QStandardItemModel 的指针,作为树形视图的数据源
};;
// mainwindow.cpp
#include "mainwindow.h"
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, treeView(new QTreeView(this)) // 创建 QTreeView 对象,并将其父对象设置为当前 MainWindow
, standardModel(new QStandardItemModel(this)) // 创建 QStandardItemModel 对象,并将其父对象设置为当前 MainWindow
{
// 将 treeView 设置为 MainWindow 的中心部件
setCentralWidget(treeView);
// 准备一行数据
QList<QStandardItem *> preparedRow = prepareRow("first", "second", "third");
// 获取模型的不可见根项(即模型的顶层项,它本身不显示)
QStandardItem *item = standardModel->invisibleRootItem();
// 向不可见根项添加一行数据,这将作为模型的根元素显示
item->appendRow(preparedRow);
// 准备第二行数据
QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
// 将第二行数据添加到第一行数据的第一个单元格对应的项下,这将创建一个子树
preparedRow[0]->appendRow(secondRow);
// 为 treeView 设置模型
treeView->setModel(standardModel);
// 展开 treeView 中的所有项
treeView->expandAll();
}
// prepareRow 成员函数,用于准备一行数据
QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
const QString &second,
const QString &third) const
{
// 创建一个包含三个 QStandardItem 对象的列表,并将它们分别初始化为 first, second, third 字符串
return {new QStandardItem(first),
new QStandardItem(second),
new QStandardItem(third)};
}
获取视图选中项
代码:[github]
下面提供的代码段旨在实现以下功能:将用户在树状视图中选定的每一项,连同其在树状结构中的层级信息(Level),共同展示在应用程序的窗口标题中。
// mainwindow.h
#include <QMainWindow>
QT_BEGIN_NAMESPACE
// 前向声明 QTreeView 类, 这对于减少编译时间和避免循环依赖很有用
class QTreeView;
class QStandardItemModel;
class QItemSelection;
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
// 一个私有槽函数,它将在 QTreeView 的选择发生变化时被调用
void selectionChangedSlot(const QItemSelection &newSelection, const QItemSelection &oldSelection);
private:
QTreeView *treeView; // 树视图
QStandardItemModel *standardModel; // 为 treeView 提供数据模型
};
#include "mainwindow.h"
#include <QTreeView>
#include <QStandardItemModel>
#include <QItemSelectionModel>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, treeView(new QTreeView(this))
, standardModel(new QStandardItemModel(this))
{
setCentralWidget(treeView);
auto *rootNode = standardModel->invisibleRootItem();
// 让我们创建几个`QStandardItem`:
auto *americaItem = new QStandardItem("America");
auto *mexicoItem = new QStandardItem("Canada");
auto *usaItem = new QStandardItem("USA");
auto *bostonItem = new QStandardItem("Boston");
auto *europeItem = new QStandardItem("Europe");
auto *italyItem = new QStandardItem("Italy");
auto *romeItem = new QStandardItem("Rome");
auto *veronaItem = new QStandardItem("Verona");
// 建立层次结构
rootNode-> appendRow(americaItem);
rootNode-> appendRow(europeItem);
americaItem-> appendRow(mexicoItem);
americaItem-> appendRow(usaItem);
usaItem-> appendRow(bostonItem);
europeItem-> appendRow(italyItem);
italyItem-> appendRow(romeItem);
italyItem-> appendRow(veronaItem);
// 将模型设置给 treeView
treeView->setModel(standardModel);
treeView->expandAll(); // 展开树视图中的所有项
// 连接选择模型的 selectionChanged 信号到 MainWindow 的 selectionChangedSlot 槽函数
QItemSelectionModel *selectionModel = treeView->selectionModel();
connect(selectionModel, &QItemSelectionModel::selectionChanged,
this, &MainWindow::selectionChangedSlot);
}
// mainwindow.cpp
void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
{
// 不使用 newSelection 和 oldSelection 参数,因此用注释标记为未使用
// 获取当前选中的项的 QModelIndex
const QModelIndex index = treeView->selectionModel()->currentIndex();
// 从 QModelIndex 中获取显示数据(通常是文本),并将其转换为 QString
QString selectedText = index.data(Qt::DisplayRole).toString();
// 初始化层级级别为 1,因为顶层项(直接挂在根节点下的项)的层级为 1
int hierarchyLevel = 1;
// 从当前选中项的 QModelIndex 开始,向上遍历其父节点,计算层级级别
QModelIndex seekRoot = index;
while (seekRoot.parent().isValid()) // 当父节点存在时继续循环
{
seekRoot = seekRoot.parent(); // 移动到父节点
hierarchyLevel++; // 每向上移动一级,层级级别加 1
}
// 构建一个包含选中项文本和层级级别的字符串
QString showString = QString("%1, Level %2").arg(selectedText).arg(hierarchyLevel);
// 将构建的字符串设置为 MainWindow 的窗口标题
setWindowTitle(showString);
}
参考:
https://doc.qt.io/qt-5/modelview.html#2-a-simple-model-view-application