• 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显示数据开始,后面慢慢扩展

    1. #include
    2. #include
    3. #include "mymodel.h"
    4. int main(int argc, char *argv[])
    5. {
    6.     QApplication a(argc, argv);
    7.     QTableView tableView;
    8.     MyModel myModel;
    9.     tableView.setModel(&myModel);
    10.     tableView.show();
    11.     return a.exec();
    12. }

    在main函数中将MyModel作为指针传递给了QTableView ,在MyModel中主要做两件事情,一是确定需要显示的行数和列数,二是需要显示到每个单元格中的内容。这里MyModel继承自QAbstractTableModel,在处理表格数据时,比继承自QAbstractItemModel更加合适。
    MyModel.h

    1. #include
    2. class MyModel : public QAbstractTableModel
    3. {
    4.     Q_OBJECT
    5. public:
    6.     MyModel(QObject *parent = nullptr);
    7.     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    8.     int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    9.     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    10. };

    MyModel.cpp

    1. #include "mymodel.h"
    2. MyModel::MyModel(QObject *parent)
    3.     : QAbstractTableModel(parent)
    4. {
    5. }
    6. int MyModel::rowCount(const QModelIndex & /*parent*/) const
    7. {
    8.    return 2;
    9. }
    10. int MyModel::columnCount(const QModelIndex & /*parent*/) const
    11. {
    12.     return 3;
    13. }
    14. QVariant MyModel::data(const QModelIndex &index, int role) const
    15. {
    16.     if (role == Qt::DisplayRole)
    17.        return QString("Row%1, Column%2")
    18.                    .arg(index.row() + 1)
    19.                    .arg(index.column() +1);
    20.     return QVariant();
    21. }

    每个单元格的数据通过参数index和role来指定,这里role使用的是Qt::DisplayRole,其他role在后面会讲到。在本例中需要显示的数据是直接生成的,但是在实际的应用中MyModel应该有个成员,比如说MyData,通过MyData来操作数据的读写。

    2.2其他的Roles

    只需要对MyModel中的data方法进行修改,根据不同的role来设置字体、背景色、布局、添加checkbox等等,就能得到丰富多彩的数据展示。

    1. QVariant MyModel::data(const QModelIndex &index, int role) const
    2. {
    3.     int row = index.row();
    4.     int col = index.column();
    5.     // generate a log message when this method gets called
    6.     qDebug() << QString("row %1, col%2, role %3")
    7.             .arg(row).arg(col).arg(role);
    8.     switch (role) {
    9.     case Qt::DisplayRole:
    10.         if (row == 0 && col == 1) return QString("<--left");
    11.         if (row == 1 && col == 1) return QString("right-->");
    12.         return QString("Row%1, Column%2")
    13.                 .arg(row + 1)
    14.                 .arg(col +1);
    15.     case Qt::FontRole:
    16.         if (row == 0 && col == 0) { //change font only for cell(0,0)
    17.             QFont boldFont;
    18.             boldFont.setBold(true);
    19.             return boldFont;
    20.         }
    21.         break;
    22.     case Qt::BackgroundRole:
    23.         if (row == 1 && col == 2)  //change background only for cell(1,2)
    24.             return QBrush(Qt::red);
    25.         break;
    26.     case Qt::TextAlignmentRole:
    27.         if (row == 1 && col == 1) //change text alignment only for cell(1,1)
    28.             return Qt::AlignRight + Qt::AlignVCenter;
    29.         break;
    30.     case Qt::CheckStateRole:
    31.         if (row == 1 && col == 0) //add a checkbox to cell(1,0)
    32.             return Qt::Checked;
    33.         break;
    34.     }
    35.     return QVariant();
    36. }

    2.3动态更新数据,显示当前时间

    需要添加一个定时器,每隔一秒更新指定单元格的数据,这里使用上面第二行第二列的单元格。timeHint是定时器的槽函数

    1. void MyModel::timerHit()
    2. {
    3.     //we identify the cell
    4.     QModelIndex index= createIndex(1,1);
    5.     //emit a signal to make the view reread identified data
    6.     emit dataChangedindex= index= {Qt::DisplayRole});
    7. }

    然后将QString("right-->");改为QTime::currentTime().toString();

    2.4设置表头

    表头是可以隐藏的tableView->verticalHeader()->hide();
    表头的内容可以通过重写headerData() 方法来修改

    1. QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
    2. {
    3.     if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
    4.         switch (section) {
    5.         case 0:
    6.             return QString("first");
    7.         case 1:
    8.             return QString("second");
    9.         case 2:
    10.             return QString("third");
    11.         }
    12.     }
    13.     return QVariant();
    14. }

    2.5编辑单元格

    需要重写setData()和flags(),setData()在编辑单元格的时候会自动调用,flags()用于调整单元格的各种特性。

    1. QVariant MyModel::data(const QModelIndex &index, int role) const
    2. {
    3.     if (role == Qt::DisplayRole && checkIndex(index))
    4.             return m_gridData[index.row()][index.column()];
    5.     return QVariant();
    6. }
    7. bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
    8. {
    9.     if (role == Qt::EditRole) {
    10.         if (!checkIndex(index))
    11.             return false;
    12.         //save value from editor to member m_gridData
    13.         m_gridData[index.row()][index.column()] = value.toString();
    14.         emit editCompleted(value.toString());
    15.         return true;
    16.     }
    17.     return false;
    18. }
    19. Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
    20. {
    21.     return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
    22. }

    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所需的标准属性。

    1. #include "mainwindow.h"
    2. #include
    3. #include
    4. #include
    5. MainWindow::MainWindow(QWidget *parent)
    6.     : QMainWindow(parent)
    7.     , treeView(new QTreeView(this))
    8.     , standardModel(new QStandardItemModel(this))
    9. {
    10.     setCentralWidget(treeView);
    11.     QList preparedRow = prepareRow("first", "second", "third");
    12.     QStandardItem *item = standardModel->invisibleRootItem();
    13.     // adding a row to the invisible root item produces a root element
    14.     item->appendRow(preparedRow);
    15.     QList secondRow = prepareRow("111", "222", "333");
    16.     // adding a row to an item starts a subtree
    17.     preparedRow.first()->appendRow(secondRow);
    18.     treeView->setModel(standardModel);
    19.     treeView->expandAll();
    20. }
    21. QList MainWindow::prepareRow(const QString &first,
    22.                                               const QString &second,
    23.                                               const QString &third) const
    24. {
    25.     return {new QStandardItem(first),
    26.             new QStandardItem(second),
    27.             new QStandardItem(third)};
    28. }

    上述代码中向不可见的根节点中添加了一行,这样就会生成一个一级节点
    接着向一级节点中添加了一个二级子节点,一颗小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

    1. #ifndef STARRATING_H
    2. #define STARRATING_H
    3. #include
    4. #include
    5. #include
    6. #include
    7. class StarRating
    8. {
    9. public:
    10.     explicit StarRating(int starCount = 1);
    11.     void paint(QPainter *painter, const QRect &rect, const QPalette &palette) const;
    12.     QSize sizeHint() const;
    13.     int starCount() const { return myStarCount; }
    14. private:
    15.     QPolygonF starPolygon;
    16.     int myStarCount;
    17. };
    18. Q_DECLARE_METATYPE(StarRating)
    19. #endif

    StarRating.cpp

    1. #include "starrating.h"
    2. const int PaintingScaleFactor = 20;
    3. StarRating::StarRating(int starCount)
    4. {
    5.     myStarCount = starCount;
    6.     starPolygon << QPointF(1.0, 0.5);
    7.     for (int i = 1; i < 5; ++i)
    8.         starPolygon << QPointF(0.5 + 0.5 * std::cos(0.8 * i * 3.14),
    9.                                0.5 + 0.5 * std::sin(0.8 * i * 3.14));
    10. }
    11. QSize StarRating::sizeHint() const
    12. {
    13.     return PaintingScaleFactor * QSize(myStarCount, 1);
    14. }
    15. void StarRating::paint(QPainter *painter, const QRect &rect, const QPalette &palette) const
    16. {
    17.     painter->save();
    18.     painter->setRenderHint(QPainter::Antialiasing, true);
    19.     painter->setPen(Qt::NoPen);
    20.     painter->setBrush(palette.foreground());
    21.     int yOffset = (rect.height() - PaintingScaleFactor) / 2;
    22.     painter->translate(rect.x(), rect.y() + yOffset);
    23.     painter->scale(PaintingScaleFactor, PaintingScaleFactor);
    24.     for (int i = 0; i < myStarCount; ++i) {
    25.         painter->drawPolygon(starPolygon, Qt::WindingFill);
    26.         painter->translate(1.0, 0.0);
    27.     }
    28.     painter->restore();
    29. }

    然后就是代理StarDelegate,代理中重写了paint和sizeHint,分别用于画图和控制尺寸
    StarDelegate.h

    1. #ifndef STARDELEGATE_H
    2. #define STARDELEGATE_H
    3. #include
    4. const int StarRole = Qt::UserRole + 1000;
    5. class StarDelegate : public QStyledItemDelegate
    6. {
    7.     Q_OBJECT
    8. public:
    9.     StarDelegate(QWidget *parent = nullptr) : QStyledItemDelegate(parent) {}
    10.     void paint(QPainter *painter, const QStyleOptionViewItem &option,
    11.                const QModelIndex &index) const override;
    12.     QSize sizeHint(const QStyleOptionViewItem &option,
    13.                    const QModelIndex &index) const override;
    14. };
    15. #endif

    StarDelegate.cpp

    1. #include "stardelegate.h"
    2. #include "starrating.h"
    3. #include
    4. void StarDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
    5.                          const QModelIndex &index) const
    6. {
    7.     if (index.data(qvariant_cast<int>(StarRole)).canConvert<StarRating>()) {
    8.         StarRating starRating = index.data(qvariant_cast<int>(StarRole)).value<StarRating>();
    9.         if (option.state & QStyle::State_Selected)
    10.             painter->fillRect(option.rect, option.palette.highlight());
    11.         starRating.paint(painter, option.rect, option.palette);
    12.     } else {
    13.         QStyledItemDelegate::paint(painter, option, index);
    14.     }
    15. }
    16. QSize StarDelegate::sizeHint(const QStyleOptionViewItem &option,
    17.                              const QModelIndex &index) const
    18. {
    19.     if (index.data(qvariant_cast<int>(StarRole)).canConvert<StarRating>()) {
    20.         StarRating starRating = index.data(qvariant_cast<int>(StarRole)).value<StarRating>();
    21.         return starRating.sizeHint();
    22.     } else {
    23.         return QStyledItemDelegate::sizeHint(option, index);
    24.     }
    25. }

    然后再上个例子的基础上添加代理,并多加一个二级子节点
     

    1. #include "mainwindow.h"
    2. #include
    3. #include
    4. #include
    5. #include "starrating.h"
    6. #include "stardelegate.h"
    7. MainWindow::MainWindow(QWidget *parent)
    8.     : QMainWindow(parent)
    9.     , treeView(new QTreeView(this))
    10.     , standardModel(new QStandardItemModel(this))
    11. {
    12.     setCentralWidget(treeView);
    13.     QList preparedRow = prepareRow("first", "second", "third");
    14.     QStandardItem *item = standardModel->invisibleRootItem();
    15.     // adding a row to the invisible root item produces a root element
    16.     item->appendRow(preparedRow);
    17.     QList secondRow = prepareRow("111", "222", "333");
    18.     // adding a row to an item starts a subtree
    19.     preparedRow.first()->appendRow(secondRow);
    20.     QList thirdRow;
    21.     for(int i=0; i<3; i++)
    22.     {
    23.         QStandardItem *starItem = new QStandardItem();
    24.         starItem->setData(QVariant::fromValue(StarRating(i+1)), StarRole);
    25.         thirdRow.append(starItem);
    26.     }
    27.     preparedRow.first()->appendRow(thirdRow);
    28.     treeView->setModel(standardModel);
    29.     treeView->setItemDelegate(new StarDelegate);
    30.     treeView->expandAll();
    31. }
    32. QList MainWindow::prepareRow(const QString &first,
    33.                                               const QString &second,
    34.                                               const QString &third) const
    35. {
    36.     return {new QStandardItem(first),
    37.             new QStandardItem(second),
    38.             new QStandardItem(third)};
    39. }

    效果图如下所示:

    参考链接: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

  • 相关阅读:
    UI自动化的适用场景,怎么做?
    Java算法探秘:二分查找详解
    17种简单有效更快地增加电子邮件列表的方法
    数仓问答篇(一)
    【DevPress】V2.4.1版本发布,增加抽奖组件
    day38:网编day5, IO多路复用
    【软考复习系列】计算机网络易错知识点记录
    电脑开机屏幕闪烁,怎么解决
    java-php-python-基于EE技术的“日进斗金”理财大师系统设计与实现计算机毕业设计
    关于idea2020.2创建springboot项目maven仓库和jdk版本不匹配大坑
  • 原文地址:https://blog.csdn.net/caoshangpa/article/details/125898303