Qt之Model/View架构
Posted 草上爬
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Qt之Model/View架构相关的知识,希望对你有一定的参考价值。
Model-View-Controller(MVC), 是从Smalltalk发展而来的一种设计模式,常被用于构建用户界面。在MVC中,模型负责获取需要显示的数据,并且存储这些数据的修改。每种数据类型都有它自己对应的模型,但是这些模型提供一个相同的API,用于隐藏内部实现。视图用于将模型数据显示给用户。对于数量很大的数据,或许只显示一小部分,这样就能很好的提高性能。控制器是模型和视图之间的媒介,将用户的动作解析成对数据的操作,比如查找数据或者修改数据,然后转发给模型执行,最后再将模型中需要被显示的数据直接转发给视图进行显示。MVC的核心思想是分层,不同的层应用不同的功能。
Qt 4 开始,引入了类似的Model/View架构来处理数据和显示之间的关系。当MVC的V和C结合在一起,我们就得到了Model/View架构。这种架构依然将数据和界面分离,但是框架更为简单。同样,这种架构也允许使用不同界面显示同一数据,也能够在不改变数据的情况下添加新的显示界面。为了处理用户输入,我们还引入了委托(delegate)。引入委托的好处是,我们能够自定义数据项的渲染和编辑。
总的来说,Model/View架构将传统的 MV 模型分为三部分:模型、视图和委托。每一个组件都由一个抽象类定义,这个抽象类提供了基本的公共接口以及一些默认实现。模型、视图和委托则使用信号槽进行交互:
☆来自模型的信号通知视图,其底层维护的数据发生了改变;
☆来自视图的信号提供了有关用户与界面进行交互的信息;
☆来自委托的信号在用户编辑数据项时使用,用于告知模型和视图编辑器的状态。
1.简介
模型/视图是一种用于从视图中分离数据的技术。标准widgets不是为从视图中分离数据而设计的,这就是为什么Qt有两种不同类型的widgets。这两种类型的widgets看起来相同,但它们与数据的交互方式不同。
☆标准widgets的数据是widgets的一部分
☆Model/View widgets操作View外部的数据(model)
1.1标准widgets
以table widget为例,table widget用于展示2D数组,用户不仅可以读取table widget提供的数据,还可以向table widget中写入数,读写操作非常方便,而且直观。但是使用table widget显示和编辑数据库中的数据可能会有问题,比如说数据库中的account数据表存了用户账号信息,不管是将数据表中的数据显示到table widget中,还是将table widget中的修改更新到数据表中,都必须协调数据的两个副本:一个在table widget中,一个在内存中。除此之外,显示和数据的紧密耦合使得编写单元测试更加困难。
1.2 Model/View widgets
Model/View提供了一个更通用的解决方案。Model/View解决了标准widgets中可能存在的数据一致性问题,不仅如此,Model/View还可以将一个Model传递给多个View,实现同一数据源的不同展示。最重要的区别是Model/View不在table的单元格中存储数据,而是通过Model直接操作数据,在Qt中,一个Model就是一个QAbstractItemModel的实现,将Model的指针传给View后,View就能读取变显示Model的内容,并成为其编辑器。
下面是Model/View widgets和相对应的标准widgets
Widget | Standard Widget (an item based convenience class) | Model/View View Class (for use with external data) |
---|---|---|
QListWidget | QListView | |
QTableWidget | QTableView | |
QTreeWidget | QTreeView | |
QColumnView shows a tree as a hierarchy of lists | ||
QComboBox can work as both a view class and also as a traditional widget |
2.一个简单的Model/View例子
例子位于Qt安装目录中:examples/widgets/tutorials/modelview
2.1 使用Qt::DisplayRole显示Model中的数据
先从QTableView显示数据开始,后面慢慢扩展
#include <QApplication>
#include <QTableView>
#include "mymodel.h"
int main(int argc, char *argv[])
QApplication a(argc, argv);
QTableView tableView;
MyModel myModel;
tableView.setModel(&myModel);
tableView.show();
return a.exec();
在main函数中将MyModel作为指针传递给了QTableView ,在MyModel中主要做两件事情,一是确定需要显示的行数和列数,二是需要显示到每个单元格中的内容。这里MyModel继承自QAbstractTableModel,在处理表格数据时,比继承自QAbstractItemModel更加合适。
MyModel.h
#include <QAbstractTableModel>
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;
;
MyModel.cpp
#include "mymodel.h"
MyModel::MyModel(QObject *parent)
: QAbstractTableModel(parent)
int MyModel::rowCount(const QModelIndex & /*parent*/) const
return 2;
int MyModel::columnCount(const QModelIndex & /*parent*/) const
return 3;
QVariant MyModel::data(const QModelIndex &index, int role) const
if (role == Qt::DisplayRole)
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() +1);
return QVariant();
每个单元格的数据通过参数index和role来指定,这里role使用的是Qt::DisplayRole,其他role在后面会讲到。在本例中需要显示的数据是直接生成的,但是在实际的应用中MyModel应该有个成员,比如说MyData,通过MyData来操作数据的读写。
2.2其他的Roles
只需要对MyModel中的data方法进行修改,根据不同的role来设置字体、背景色、布局、添加checkbox等等,就能得到丰富多彩的数据展示。
QVariant MyModel::data(const QModelIndex &index, int role) const
int row = index.row();
int col = index.column();
// generate a log message when this method gets called
qDebug() << QString("row %1, col%2, role %3")
.arg(row).arg(col).arg(role);
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:
if (row == 0 && col == 0) //change font only for cell(0,0)
QFont boldFont;
boldFont.setBold(true);
return boldFont;
break;
case Qt::BackgroundRole:
if (row == 1 && col == 2) //change background only for cell(1,2)
return QBrush(Qt::red);
break;
case Qt::TextAlignmentRole:
if (row == 1 && col == 1) //change text alignment only for cell(1,1)
return Qt::AlignRight + Qt::AlignVCenter;
break;
case Qt::CheckStateRole:
if (row == 1 && col == 0) //add a checkbox to cell(1,0)
return Qt::Checked;
break;
return QVariant();
2.3动态更新数据,显示当前时间
需要添加一个定时器,每隔一秒更新指定单元格的数据,这里使用上面第二行第二列的单元格。timeHint是定时器的槽函数
void MyModel::timerHit()
//we identify the cell
QModelIndex index= createIndex(1,1);
//emit a signal to make the view reread identified data
emit dataChangedindex= index= Qt::DisplayRole);
然后将QString("right-->");改为QTime::currentTime().toString();
2.4设置表头
表头是可以隐藏的tableView->verticalHeader()->hide();
表头的内容可以通过重写headerData() 方法来修改
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");
return QVariant();
2.5编辑单元格
需要重写setData()和flags(),setData()在编辑单元格的时候会自动调用,flags()用于调整单元格的各种特性。
QVariant MyModel::data(const QModelIndex &index, int role) const
if (role == Qt::DisplayRole && checkIndex(index))
return m_gridData[index.row()][index.column()];
return QVariant();
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
if (role == Qt::EditRole)
if (!checkIndex(index))
return false;
//save value from editor to member m_gridData
m_gridData[index.row()][index.column()] = value.toString();
emit editCompleted(value.toString());
return true;
return false;
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
m_gridData是一个二维QString数组,QString m_gridData[2][3],用于存放编辑后的数据。信号editCompleted(const QString &);将单元格的改动通知到上层,这样在上层绑定该信号就可以获取到改动后的数据
connect(myModel, &MyModel::editCompleted, this, &XXXXX::XXX);
3.TreeView
将上面例子的QTableView替换成QTreeView,将会得到一个可读可写的tree view应用程序,不需要对model做任何改动,只不过此时的树没有任何的层次结构,因为model本身没有任何层次结构。
QListView、QTableView和QTreeView可以用同一模型来抽象,该模型合并了list、table和tree的特性。这使得可以将同一Model用于几种不同类型的View。
抽象后的模型如下
如果要实现一颗真正的tree(有层次结构),上面例子里用的MyModel显示不满足要求,这次我们用QStandardItemModel,QStandardItemModel是QAbstractItemModel的一个实现,要显示tree,QStandardItemModel必须填充QStandardItem,QStandardItem能够保存文本、字体、复选框或画笔等Item所需的标准属性。
#include "mainwindow.h"
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, treeView(new QTreeView(this))
, standardModel(new QStandardItemModel(this))
setCentralWidget(treeView);
QList<QStandardItem *> preparedRow = prepareRow("first", "second", "third");
QStandardItem *item = standardModel->invisibleRootItem();
// adding a row to the invisible root item produces a root element
item->appendRow(preparedRow);
QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
// adding a row to an item starts a subtree
preparedRow.first()->appendRow(secondRow);
treeView->setModel(standardModel);
treeView->expandAll();
QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
const QString &second,
const QString &third) const
return new QStandardItem(first),
new QStandardItem(second),
new QStandardItem(third);
上述代码中向不可见的根节点中添加了一行,这样就会生成一个一级节点
接着向一级节点中添加了一个二级子节点,一颗小tree就这样形成了,如下图所示
下面是Qt提供的一些已经定义好了的Model
QStringListModel | Stores a list of strings |
QStandardItemModel | Stores arbitrary hierarchical items |
QFileSystemModel | Encapsulate the local file system |
QSqlQueryModel | Encapsulate an SQL result set |
QSqlTableModel | Encapsulates an SQL table |
QSqlRelationalTableModel | Encapsulates an SQL table with foreign keys |
QSortFilterProxyModel | Sorts and/or filters another model |
4.delegate
到目前为止单元格中操作的数据都是文本和checkbox,这些提供显示和编辑的组件统称为委托(delegate),其实前面已经用到了delegate,只不过该delegate是view默认的delegate,下面我们要自定义一个图形化的Start Delegate,五角星的多少标识评级的高低。
首先需要一个标识五角星的类StarRating
StarRating.h
#ifndef STARRATING_H
#define STARRATING_H
#include <QMetaType>
#include <QPointF>
#include <QVector>
#include <QPainter>
class StarRating
public:
explicit StarRating(int starCount = 1);
void paint(QPainter *painter, const QRect &rect, const QPalette &palette) const;
QSize sizeHint() const;
int starCount() const return myStarCount;
private:
QPolygonF starPolygon;
int myStarCount;
;
Q_DECLARE_METATYPE(StarRating)
#endif
StarRating.cpp
#include "starrating.h"
const int PaintingScaleFactor = 20;
StarRating::StarRating(int starCount)
myStarCount = starCount;
starPolygon << QPointF(1.0, 0.5);
for (int i = 1; i < 5; ++i)
starPolygon << QPointF(0.5 + 0.5 * std::cos(0.8 * i * 3.14),
0.5 + 0.5 * std::sin(0.8 * i * 3.14));
QSize StarRating::sizeHint() const
return PaintingScaleFactor * QSize(myStarCount, 1);
void StarRating::paint(QPainter *painter, const QRect &rect, const QPalette &palette) const
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(Qt::NoPen);
painter->setBrush(palette.foreground());
int yOffset = (rect.height() - PaintingScaleFactor) / 2;
painter->translate(rect.x(), rect.y() + yOffset);
painter->scale(PaintingScaleFactor, PaintingScaleFactor);
for (int i = 0; i < myStarCount; ++i)
painter->drawPolygon(starPolygon, Qt::WindingFill);
painter->translate(1.0, 0.0);
painter->restore();
然后就是代理StarDelegate,代理中重写了paint和sizeHint,分别用于画图和控制尺寸
StarDelegate.h
#ifndef STARDELEGATE_H
#define STARDELEGATE_H
#include <QStyledItemDelegate>
const int StarRole = Qt::UserRole + 1000;
class StarDelegate : public QStyledItemDelegate
Q_OBJECT
public:
StarDelegate(QWidget *parent = nullptr) : QStyledItemDelegate(parent)
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
;
#endif
StarDelegate.cpp
#include "stardelegate.h"
#include "starrating.h"
#include <QDebug>
void StarDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
if (index.data(qvariant_cast<int>(StarRole)).canConvert<StarRating>())
StarRating starRating = index.data(qvariant_cast<int>(StarRole)).value<StarRating>();
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, option.palette.highlight());
starRating.paint(painter, option.rect, option.palette);
else
QStyledItemDelegate::paint(painter, option, index);
QSize StarDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
if (index.data(qvariant_cast<int>(StarRole)).canConvert<StarRating>())
StarRating starRating = index.data(qvariant_cast<int>(StarRole)).value<StarRating>();
return starRating.sizeHint();
else
return QStyledItemDelegate::sizeHint(option, index);
然后再上个例子的基础上添加代理,并多加一个二级子节点
#include "mainwindow.h"
#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>
#include "starrating.h"
#include "stardelegate.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, treeView(new QTreeView(this))
, standardModel(new QStandardItemModel(this))
setCentralWidget(treeView);
QList<QStandardItem *> preparedRow = prepareRow("first", "second", "third");
QStandardItem *item = standardModel->invisibleRootItem();
// adding a row to the invisible root item produces a root element
item->appendRow(preparedRow);
QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
// adding a row to an item starts a subtree
preparedRow.first()->appendRow(secondRow);
QList<QStandardItem *> thirdRow;
for(int i=0; i<3; i++)
QStandardItem *starItem = new QStandardItem();
starItem->setData(QVariant::fromValue(StarRating(i+1)), StarRole);
thirdRow.append(starItem);
preparedRow.first()->appendRow(thirdRow);
treeView->setModel(standardModel);
treeView->setItemDelegate(new StarDelegate);
treeView->expandAll();
QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
const QString &second,
const QString &third) const
return new QStandardItem(first),
new QStandardItem(second),
new QStandardItem(third);
效果图如下所示:
参考链接:https://doc.qt.io/qt-6/model-view-programming.html
参考链接:https://doc.qt.io/qt-6/modelview.html#1-1-standard-widgets
原文链接:https://blog.csdn.net/caoshangpa/article/details/125898303
以上是关于Qt之Model/View架构的主要内容,如果未能解决你的问题,请参考以下文章