Qt模型/视图教程

模型/视图教程1


每个UI开发者都应该要知道 模型/视图(Model/View)编程,本教程提供了一个关于该话题的一个易于理解的引导。

表格,列表和树widget是在GUI编程中频繁使用的组件。这些widget有两种不同的方式去访问它们的数据。传统的方式是使用内部存有数据的widget,这种方式非常直观,但是在许多特殊的应用程序中,它会导致数据同步问题。第二种方式是模型/视图编程,widget内部不包含存放数据的容器,这些widget通过一个标准的接口去访问外部数据,因此避免了数据冗余。这看起来似乎很复杂,但是一旦你深入研究,它不但易于掌握,而且模型/视图编程的许多好处都将慢慢呈现。

img

在这个过程中,我们将学习一些Qt的基本技术,例如:

  • 标准widget和模型/视图widget之间的差异
  • 表单和模型之间的适配器
  • 开发一个简单的模型/视图应用程序
  • 预定义的模型
  • 进阶话题例如:
    • 树视图
    • 选择
    • 委托
    • 模块测试调试

你也将学习到,你的应用程序能否采用模型/视图编程简化开发,是否传统的widget将工作地更好。

这个教程为你提供了示例代码,让你能够编辑和集成它们到你的项目中。这个教程地源代码位于Qt目录下的examples/widgets/tutorials/modelview

获取更多有关模型/视图编程的详细信息,请参考reference documentation

1. 引入

模型/视图是一个技术,用于将数据和视图分离。标准的widget不被设计为数据和视图分离,这也是Qt有两种不同类型的widget的原因。两种类型的widget看上去是相同的,但是它们和数据的交互方式不同。

标准的widget数据是widget的一部分img
视图类操作外部数据(模型)img

1.1 标准widget

让我们深入了解一下标准的表格widget。一个表格widget是一个数据元素的2维数组,用户可以更改这些数据元素。通过读写表格widget中的数据元素,表格widget可以集成到程序流中。这种方式非常直观并且在许多应用程序中都是有用的,但是,通过一个标准的表格widget显示和编辑一个数据库表,会造成一些问题。两份数据的拷贝需要被协调:一份在widget外部;另一份在widget内部。开发者需要同步这两个版本。除此之外,数据的呈现和数据的紧密耦合使得编写单元测试更加困难。

1.2 模型/视图提供解决方案

模型/视图使用了一种更加优雅的方式提供了解决方案。模型/视图消除了在标准widget上的数据一致性问题。模型/视图也让,同时使用多个相同数据的视图,使用起来更简单,因为一个模型可以被传递给许多视图。最重要的差异是模型/视图widget不存储数据。事实上,它们直接操作你的数据。由于视图类不知道你的数据结构,所以你需要提供一个包装器,让你的数据满足QAbstractItemModel接口的要求。一个视图使用这些接口去读写你的数据。任何实现的QAbstractItemModel接口的类的对象,都被成为模型。一旦视图接收到了一个指向模型的指针,视图将读取和显示模型的内容,并成为模型的编辑器。

1.3 模型/视图widget概览

这是一个模型/视图widget和它们相对应的标准widget的概览

Widget标准widget模型/视图的视图类
imgQListWidgetQListView
imgQTableWidgetQTableView
imgQTreeWidgetQTreeView
imgQColumnView 展示树层级结构的列表
imgQComboBox 可以以视图类或传统widget工作

1.4 在表单和模型间使用适配器

在表单和模型之间使用适配器会很方便。

我们可以直接在表格中编辑存储在表格中的数据,但是在文本字段中编辑数据会更舒适。没有直接的模型/视图???

QDataWidgetMapper是一个很好的解决方案,因为它映射表单widget到表格的行,并使非常容易为数据库表构建表单。

img

另一个适配器的例子是QCompleterQCompleter提供Qt widegt中的自动完成,例如QComboBox和下面展示的QLineEditQCompleter使用一个模型作为他的数据源。

img

2. 一个简单的模型/视图应用程序

如果你想开发一个模型/视图应用程序,你应该从何处开始?我们推荐从一个简单的案例开始,并且逐步扩展它。这使得理解次架构非常简单。事实证明,在调用IDE之前尝试去理解模型/视图架构的细节对许多开发人员来说并不方便。通过一个简单的模型/视图应用程序作为开始实际上会更简单。试试看!将示例中的数据替换成你自己的数据。

下面有7个非常简单并且独立的应用程序,它们展示的模型/视图编程的不同方面。源代码可以在examples/widgets/tutorials/modelview目录中找到。

2.1 一个只读的表

我们通过一个使用QTableView展示数据的应用程序开始。我们将在之后给他提供编辑能力。

(文件源码: examples/widgets/tutorials/modelview/1_readonly/main.cpp)

// main.cpp
#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()函数:

这是有趣的部分:我们通过tableView.setModel(&myModel);创建了一个MyModel的实例,然后使用tableView.setModel(&myModel);传递给ableView一个指针。ableView将调用这个指针的方法去得到两个东西:

  • 应该显示多少行和多少列
  • 在每个单元格中应该打印什么内容

模型需要一些代码对此做出回应。

我们有一个表格数据,让我们从QAbstractTableModel开始,因为它比普通的QAbstractItemModel更容易使用。

(文件源码: examples/widgets/tutorials/modelview/1_readonly/mymodel.h)

// 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;
};

QAbstractTableModel需要实现三个抽象方法。

(文件源码: examples/widgets/tutorials/modelview/1_readonly/mymodel.cpp)

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

行数和列数被MyModel::rowCount()MyModel::columnCount()提供。当视图想知道表格的文本时,它将调用MyModel::data()。行和列的信息被参数index指定,同时角色被设置为Qt::DisplayRole。其他的角色将在下一节中讲解。在我们的例子中,被显示的数据是手动生成的,但在实际的应用程序中,MyModel将有一个名为MyData的成员,他作为所有读写操作的目标。

这个小例子说明了模型的被动性质。模型不知道它们自己什么时候被使用,或者需要什么数据。它只在视图每次需要时提供数据。

当模型的数据需要改变时,什么发生了?视图如何意识到已更改的数据需要再次读取?模型必须发出一个信号,该信号指示已更改了哪些单元格。这些内容将在2.3节中被说明。

2.2 使用角色(Role)扩展只读的案例

除了控制视图显示的文本之外,模型还控制了这些文本的外观。当我们稍微改变模型时,我们得到以下结果:

img

事实上,除了data()方法外,无需再更改其他任何内容即可设置字体,背景色,对齐方式和复选框。下面是产生上面结果的data()方法。差异在于这次我们使用了int参数来返回不同的信息。

(文件源码: examples/widgets/tutorials/modelview/2_formatting/mymodel.cpp)

// mymodel.cpp
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();
}

通过对data()方法单独的调用从模型请求每个格式设置的属性。role参数用于让模型知道什么属性被请求:

enum Qt::ItemDataRoleMeaningType
Qt::DisplayRole文本QString
Qt::FontRole字体QFont
BackgroundRolebrush for the background of the cellQBrush
Qt::TextAlignmentRole文本对齐enum Qt::AlignmentFlag
Qt::CheckStateRole复选框enum Qt::ItemDataRole

参看Qt命名空间文档以了解更多有关Qt::ItemDataRole的枚举值

现在我们需要确定使用分离的模型如何影响应用程序的性能,因此让我们跟踪视图调用data()方法的频率。为了追踪视图调用模型的频率,我们在data()方法中添加了一个调式语句。在我们的小例子中,data()将被调用42次。每当你将光标悬停在字段上时,data()将被调用–每个单元格调用7次。这就是为什么在调用data()和缓存昂贵的查找操作时,确保你的数据可用的重要原因。

2.3 将钟嵌入表的单元格

img

我们仍然有一个只读的表格,但是这次内容会被秒更新一次,因为我们显示的是当前时间。

(文件源码: examples/widgets/tutorials/modelview/3_changingmodel/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();
}

缺少一些东西来使时钟滴答作响。我们需要告诉视图时间已更改,并且需要再次读取。我们通过计时器来完成这个操作。在构造函数中,设置计时器的间隔为1秒然后连接他的timeout信号。

(文件源码: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
    , timer(new QTimer(this))
{
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout , this, &MyModel::timerHit);
    timer->start();
}

这是相应的槽:

void MyModel::timerHit()
{
    // 标识左上角单元格
    QModelIndex topLeft = createIndex(0,0);
    // 发生信号使视图重新读取被标识的数据
    emit dataChanged(topLeft, topLeft, {Qt::DisplayRole});
}

我们请求视图重新读取在左上角单元格的数据通过发射dataChanged()信号。主义我们没有显式连接dataChanged()信号到视图。这将在我们调用setModel()时自动完成。

2.4 设置列和行标题

标题可以通过视图的tableView->verticalHeader()->hide();方法来隐藏。

img

然而,标题的内容是通过模型设置的,所以我们重写了headerData()方法:

(文件源码: examples/widgets/tutorials/modelview/4_headers/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");
        }
    }
    return QVariant();
}

注意,方法headerData()也有一个角色参数,他和MyModel::data()中的含义相同。

2.5 小的编辑案例

在这个例子中,我们将构建一个应用程序,它通过单元格中的值自动填充窗口标题。为了访问窗口标题更简单,我们将QTableView放在一个QMainWindow中。

模型决定了是否可编辑内容。我们只需要修改模型即可启用编辑功能。通过重写以下虚函数来实现该目标:setData()flags()

(文件源码: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

// 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];  // 维护文本内容
signals:
    void editCompleted(const QString &);
};

我们使用二维数组m_gridData来存储我们的数据。这让m_gridData成为了MyModel的核心。MyModel的其余部分就像包装器一样,将m_gridData调整为QAbstractItemModel接口。我们还引入了editCompleted()信号,这样就可以将修改后的文本传输到窗口标题。

(文件源码: examples/widgets/tutorials/modelview/5_edit/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();
        // 只是为了显示目的:构建和发射一个被连接的字符串
        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提供编辑的结果。角色将总是被设置为Qt::EditRole因为我们的单元格只包含文本。如果存在一个复选框,并且设置了权限以允许选中该复选框,则还会将角色设置为Qt::CheckStateRole进行调用。

(文件源码: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

一个单元格的多种属性可以通过flags()进行调整。

返回Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled足以向编辑器显示可以选择一个单元格。

如果编辑一个单元格所修改的数据多于该特定单元格中的数据,则该模型必须发射一个dataChanged()信号,以便读取已更改的数据。

3. 进阶话题

3.1 TreeView

你可以使用一个树视图将上面的例子转换为一个应用程序。只需要将TableView替换为QTreeView。无需对模型进行任何更改。该树没有任何层次结构,因为模型本身没有任何层次结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zldSAfVL-1587833727012)(https://doc.qt.io/qt-5/images/dummy_tree.png)]

QListViewQTableViewQTreeView都是用一个抽象的模型,即合并的列表,表格和树。这样就可以使用同一模型的几种不同类型的视图类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QzmqTZZd-1587833727012)(https://doc.qt.io/qt-5/images/list_table_tree.png)]

到目前为止,这是示例模型的外观:

img

我们想呈现一棵真实的树。我们在上面的示例中封装了数据以建立模型。这次我们使用QStandardItemModel,它是一个用于分层数据的容器,该容器还实现了QAbstractItemModel。要显示一棵树,QStandardItemModel必须使用QStandardItem来填充,QStandardItem能够维护所有项的标准属性,例如文本,字体,复选框和画刷。

img

(文件源码: examples/widgets/tutorials/modelview/6_treeview/mainwindow.cpp)

// modelview.cpp
#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();
    // 添加一行到不可见的根项,以产生一个根元素
    item->appendRow(preparedRow);

    QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
    // 添加一行到一个项,开启一颗子树
    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)};
}

我们只需要实例化一个QStandardItemModel并向构造函数添加几个QStandardItems。然后,我们可以建立分层数据结构,因为QStandardItem可以容纳其他QStandardItem。节点在视图内折叠并展开。

3.2 处理选择

我们想访问被选择的项的内容,以便将其与层次结构级别一同输出到窗口标题中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjFhr7DC-1587833727014)(https://doc.qt.io/qt-5/images/selection2.png)]

所以,让我们创建几个项:

(文件源码: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

#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);
    QStandardItem *rootNode = standardModel->invisibleRootItem();

    // 定义几个项
    QStandardItem *americaItem = new QStandardItem("America");
    QStandardItem *mexicoItem =  new QStandardItem("Canada");
    QStandardItem *usaItem =     new QStandardItem("USA");
    QStandardItem *bostonItem =  new QStandardItem("Boston");
    QStandardItem *europeItem =  new QStandardItem("Europe");
    QStandardItem *italyItem =   new QStandardItem("Italy");
    QStandardItem *romeItem =    new QStandardItem("Rome");
    QStandardItem *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->setModel(standardModel);
    treeView->expandAll();

    // 选择更改将出发槽函数
    QItemSelectionModel *selectionModel = treeView->selectionModel();
    connect(selectionModel, &QItemSelectionModel::selectionChanged,
            this, &MainWindow::selectionChangedSlot);
}

视图在单独的选择模型中管理选择,可以使用selectionModel()方法进行检索。我们检索选择模型以连接一个槽到它的selectionChanged()信号。

(文件源码: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
{
    // 得到被选择的项的文本
    const QModelIndex index = treeView->selectionModel()->currentIndex();
    QString selectedText = index.data(Qt::DisplayRole).toString();
    // 找出所选项目的层级结构级别
    int hierarchyLevel = 1;
    QModelIndex seekRoot = index;
    while (seekRoot.parent() != QModelIndex()) {
        seekRoot = seekRoot.parent();
        hierarchyLevel++;
    }
    QString showString = QString("%1, Level %2").arg(selectedText)
                         .arg(hierarchyLevel);
    setWindowTitle(showString);
}

我们通过调用treeView->selectionModel()->currentIndex()获得与选择相对应的模型索引,同时我们得到了字段的字符串通过使用模型索引。然后我们计算了项的层级hierarchyLevel。顶级项没有父节点并且parent()将返回一个默认构造的QModelIndex()。这是我们在计算迭代期间执行的步骤时使用parent()方法迭代到最高级别的原因。

选择模型(如上面显示的)可以被检索,但是他也能同过QAbstractItemView::setSelectionModel来设置。这样就可以使3个视图类具有同步的选择,因为仅使用了一个选择模型实例。使用selectionModel()并通过setSelectionModel()将结果分配给第二个和第三个视图类,在3个视图之间共享选择模型。

3.3 预定义的模型

使用模型/视图的典型方法是包装特定数据以使其可用于视图类。然而Qt也提供了预定义的模型用于基础的数据结构。如果可用的数据结构之一适合你的应用程序,那么预定义的模型可能是一个不错的选择。

QStringListModel存储字符串列表
QStandardItemModel存储任意分层项
QFileSystemModel QDirModel封装本地文件系统
QSqlQueryModel封装SQL结果集
QSqlTableModel封装SQL表
QSqlRelationalTableModel使用外键封装SQL表
QSortFilterProxyModel排序和/或过滤另一个模型

3.4 委托

到目前位置的所有例子,数据都以文本或复选框在表格中表示和编辑。提供这些表示和编辑的服务组件称为委托。我们只是刚刚开始使用委托,因为视图使用了一个默认的委托。但是,假设我们想要一个不同的编辑器(例如滑块过下拉列表),假设我们想要以图片表示数据。让我们看一个名为Star Delegate的示例,其中的星星用于显示等级:

img

视图有一个setItemDelegate()方法,它替换默认的委托然后安装一个自定义的委托。可以通过创建从QStyledItemDelegate继承的类来编写新的委托。为了编写一个显示星星且没有输入能力的委托,我们只需要重写两个方法。

class StarDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    StarDelegate(QWidget *parent = 0);
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const;
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const;
};

paint()依赖底层的数据内容来绘制星星。可以通过调用index.data()来查找数据。委托的sizeHint()方法用于获得每个星星的尺寸,以便单元格提供足够的宽高去容纳这些星星。

如果你想要在视图类的网格内使用自定义的图形展示你的数据,则编写自定义的委托是正确的选择。如果你不想要网格,则不需要使用自定义的委托而是使用自定义的视图类。

在Qt文档中的其他和委托相关的参考:

3.5 ModelTest和调式

模型的被动性质为程序员带来了新的挑战。模型中的不一致会导致应用程序崩溃。由于模型受到了视图无数次的调用,因此很难找出那个调用使应用程序崩溃了,以及哪个操作导致了该问题。

Qt Labs提供了称为ModelTest的软件,他将在你的程序运行时检查模型。每当模型改变时,ModelTest将扫描此模型并报告断言错误。这对树模型来说非常重要,因为它们的层级结构为细微的不一致留下了许多可能。

不像视图类,ModelTest使用范围外的索引来测试模型。这意味着你的应用程序可能因为ModelTest崩溃,即使在没有ModelTest时运行的很好。因此在使用ModelTest时,你还需要处理所有超出范围的索引。
alDelegate Class Reference](https://doc.qt.io/qt-5/qsqlrelationaldelegate.html)

3.5 ModelTest和调式

模型的被动性质为程序员带来了新的挑战。模型中的不一致会导致应用程序崩溃。由于模型受到了视图无数次的调用,因此很难找出那个调用使应用程序崩溃了,以及哪个操作导致了该问题。

Qt Labs提供了称为ModelTest的软件,他将在你的程序运行时检查模型。每当模型改变时,ModelTest将扫描此模型并报告断言错误。这对树模型来说非常重要,因为它们的层级结构为细微的不一致留下了许多可能。

不像视图类,ModelTest使用范围外的索引来测试模型。这意味着你的应用程序可能因为ModelTest崩溃,即使在没有ModelTest时运行的很好。因此在使用ModelTest时,你还需要处理所有超出范围的索引。


  1. 翻译自Qt官方文档https://doc.qt.io/qt-5/modelview.html ↩︎

相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页