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

WidgetStandard Widget
(an item based convenience class)
Model/View View Class
(for use with external data)
QListWidgetQListView
QTableWidgetQTableView
QTreeWidgetQTreeView
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

QStringListModelStores a list of strings
QStandardItemModelStores arbitrary hierarchical items
QFileSystemModelEncapsulate the local file system
QSqlQueryModelEncapsulate an SQL result set
QSqlTableModelEncapsulates an SQL table
QSqlRelationalTableModelEncapsulates an SQL table with foreign keys
QSortFilterProxyModelSorts 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架构的主要内容,如果未能解决你的问题,请参考以下文章

QT Model View Controller 使用和认识

QT开发(三十六)——Model/View框架

Qt之QListView使用

5.Qt model view设计模式

QT开发(三十八)——Model/View框架编程

Android应用架构之Android MVP使用