0
点赞
收藏
分享

微信扫一扫

Qt之Model/View架构


Model-View-Controller(MVC), 是从Smalltalk发展而来的一种设计模式,常被用于构建用户界面。在MVC中,模型负责获取需要显示的数据,并且存储这些数据的修改。每种数据类型都有它自己对应的模型,但是这些模型提供一个相同的API,用于隐藏内部实现。视图用于将模型数据显示给用户。对于数量很大的数据,或许只显示一小部分,这样就能很好的提高性能。控制器是模型和视图之间的媒介,将用户的动作解析成对数据的操作,比如查找数据或者修改数据,然后转发给模型执行,最后再将模型中需要被显示的数据直接转发给视图进行显示。MVC的核心思想是分层,不同的层应用不同的功能。
Qt 4 开始,引入了类似的Model/View架构来处理数据和显示之间的关系。当MVC的V和C结合在一起,我们就得到了Model/View架构。这种架构依然将数据和界面分离,但是框架更为简单。同样,这种架构也允许使用不同界面显示同一数据,也能够在不改变数据的情况下添加新的显示界面。为了处理用户输入,我们还引入了委托(delegate)。引入委托的好处是,我们能够自定义数据项的渲染和编辑。

Qt之Model/View架构_委托


 总的来说,Model/View架构将传统的 MV 模型分为三部分:模型、视图和委托。每一个组件都由一个抽象类定义,这个抽象类提供了基本的公共接口以及一些默认实现。模型、视图和委托则使用信号槽进行交互:

☆来自模型的信号通知视图,其底层维护的数据发生了改变;

☆来自视图的信号提供了有关用户与界面进行交互的信息;

☆来自委托的信号在用户编辑数据项时使用,用于告知模型和视图编辑器的状态。

1.简介

模型/视图是一种用于从视图中分离数据的技术。标准widgets不是为从视图中分离数据而设计的,这就是为什么Qt有两种不同类型的widgets。这两种类型的widgets看起来相同,但它们与数据的交互方式不同。
☆标准widgets的数据是widgets的一部分

Qt之Model/View架构_qt_02


☆Model/View widgets操作View外部的数据(model) 

Qt之Model/View架构_qt_03

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)

Qt之Model/View架构_委托_04

​​QListWidget​​

​​QListView​​

Qt之Model/View架构_委托_05

​​QTableWidget​​

​​QTableView​​

Qt之Model/View架构_qt_06

​​QTreeWidget​​

​​QTreeView​​

Qt之Model/View架构_委托_07

​​QColumnView​​ shows a tree as a hierarchy of lists

Qt之Model/View架构_qt_08

​​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来操作数据的读写。

Qt之Model/View架构_mvc_09

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();

Qt之Model/View架构_delegate_10

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。

Qt之Model/View架构_delegate_11

 抽象后的模型如下

Qt之Model/View架构_Model/View_12


如果要实现一颗真正的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/View架构_Model/View_13

Qt之Model/View架构_委托_14


下面是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)};
}

效果图如下所示:

Qt之Model/View架构_mvc_15

参考链接:​​https://doc.qt.io/qt-6/model-view-programming.html​​

参考链接:​​https://doc.qt.io/qt-6/modelview.html#1-1-standard-widgets​​

举报

相关推荐

0 条评论