修正版已转移到 Qt中文文档

前言

该文章翻译的官方文档 原文: https://doc.qt.io/qt-5/modelview.html

Model/View 说明

  每个UI开发人员都应该了解Mode/View编程,本教程将对Model/View进行全面的介绍。
  表(Table)、列表(List)和树(Tree)控件是gui中经常使用的控件。这些控件访问数据的方式有两种。比较传统的方法是控件包含一个用于存储数据的内部容器。这种方法非常直观,但是,在许多重要的应用程序中,它会导致数据同步问题。第二种方法是Model/View编程,其中控件不维护内部数据容器。它们通过标准化接口访问外部数据,因此避免了数据重复。乍一看,这似乎很复杂,但是一旦您仔细观察,就会发现它不仅容易掌握,而且Mode/View编程的许多好处也变得更加清晰。

在这个过程中,我们将了解到Qt提供的一些基本技术,例如:
  • 标准控件与Mode/View控件的区别
  • 窗体和模型之间的适配器
  • 开发一个简单的Mode/View的应用程序
  • 预定义模型
  • 中级主题比如:
    • 树形视图(Tree views)
    • 选项 (Selection)
    • 代理(Delegates)
    • 模型试验调试(Debugging with model test)

   您还将了解到新的应用程序是否可以通过Mode/View编程更容易地编写,或者经典的标准控件是否也可以工作。本教程包括用于编辑和集成到项目中的示例代码。教程的源代码位于qt的examples/widgets/tutorial s/modelview目录中。
更多详细信息,参照:

1.介绍

   Mode/View是一种用于在处理数据集的控件中分离数据和视图的技术。标准控件不是为将数据与视图分离而设计的,这就是为什么Qt有两种不同类型的控件。两种类型的控件看起来都一样,但它们与数据的交互方式不同。

标准控件使用的数据是作为控件一部分的
视图(View)类操作外部数据(模型)

1.1 标准控件

  让我们更仔细地看一看标准的表格控件。表格控件是用户可以更改的数据元素的二维数组。通过读写表格控件提供的数据元素,表格控件可以集成到程序流中。这种方法在许多应用程序中非常直观和有用,但是使用标准表格控件显示和编辑数据库表可能会有问题。必须协调两个数据副本:一个在控件外部;一个在控件内部。开发人员负责同步两个版本。除此之外,表示和数据的紧密耦合使得编写单元测试更加困难。

1.2 使用Model/View

  Mode/View提供了一个使用更通用的体系结构的解决方案。Mode/View消除了标准控件可能出现的数据一致性问题。Mode/View还可以更容易地使用同一数据的多个视图,因为一个模型可以传递给多个视图。最重要的区别是Mode/View控件不在表单元格后面存储数据。实际上,它们直接从您的数据操作。因为视图类不知道数据的结构,所以需要提供一个包装器,使数据符合QabstracteModel接口。视图使用此接口读取和写入数据。实现QabstratemModel的类的任何实例都称为模型。一旦视图接收到指向模型的指针,它将读取和显示其内容,并成为其编辑器。

1.3 Model/View 控件概览

下面是Model/View 控件及其相应标准控件。

控件标准控件Model/View 视图类
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

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

  在窗体和模型之间使用适配器非常方便。我们可以直接从表内部编辑存储在表中的数据,但是在文本字段中编辑数据更为方便。没有直接的Model/View能够在对控件的一个数据而不是数据集操作时将数据和视图分离开。比如QLineEdit,QCheckBox…因此我们需要一个适配器来将表单连接到数据源。
  QdataWidgetMapper是一个很好的解决方案,因为它将表单控件映射到表行,并使为数据库表构建表单变得非常容易。

  另一个是QCompleter。 Qt中的QCompleter能够自动完成适配,例如QComboBox,如下所示,QLineEdit。 QCompleter使用模型作为其数据源。

2. 一个简单的 Model/View 应用程序

  如果要开发模Model/View 应用程序,应该从哪里开始? 我们建议从一个简单的示例开始并逐步扩展它。 这使得理解架构变得更加容易。 在调用IDE之前,尝试详细了解Model/View 体系结构已被证明对许多开发人员来说不太方便。 从具有演示数据的简单Model/View 应用程序开始,实质上更容易。 试试看! 只需将您自己的数据替换为以下示例中的数据即可。
下面是7个非常简单和独立的应用程序,它们显示了Model/View 编程的不同方面。 源代码可以在examples / widgets / tutorials / modelview目录中找到。

2.1 只读表

我们从使用QTableView显示数据的应用程序开始。 我们稍后会添加编辑功能。
(文件来源:examples / widgets / tutorials / modelview / 1_readonly / main.cpp)

  // main.cpp
  #include <QtWidgets/QApplication>
  #include <QtWidgets/QTableView>
  #include "mymodel.h"

  int main(int argc, char *argv[])
  {
      QApplication a(argc, argv);
      QTableView tableView;
      MyModel myModel(0);
      tableView.setModel( &myModel );
      tableView.show();
      return a.exec();
  }

main()函数:
这是有趣的部分:我们创建一个MyModel实例并使用tableView.setModel(&myModel); 将它的指针传递给tableView。 tableView将调用它收到的指针的方法来找出两件事:

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

该模型需要一些代码来响应这一点。
我们有一个表数据集,所以让我们从QAbstractTableModel开始,因为它比更通用的QAbstractItemModel更容易使用。
(文件来源:examples / widgets / tutorials / modelview / 1_readonly / mymodel.h)

  // mymodel.h
  #include <QAbstractTableModel>

  class MyModel : public QAbstractTableModel
  {
      Q_OBJECT
  public:
      MyModel(QObject *parent);
      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()。 使用参数索引指定行和列信息,并将角色设置为Qt :: DisplayRole。 其他角色将在下一节中介绍。 在我们的示例中,生成了应显示的数据。 在实际的应用程序中,MyModel将有一个名为MyData的成员,它作为所有读写操作的目标。
  这个小例子展示了模型的被动性。 该模型不知道何时使用或需要哪些数据。 它只是在每次视图请求时提供数据。
当模型的数据需要更改时会发生什么? 视图如何实现数据已更改并需要再次读取? 该模型必须发出一个信号,指示哪些单元格范围已经改变。 这将在2.3节中说明。

2.2 使用角色扩展只读表格示例

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

  事实上,除了data()方法外,不需要更改任何内容来设置字体、背景颜色、对齐方式和复选框。下面是产生上面所示结果的data()方法。不同的是,这次我们使用参数int role根据其值返回不同的信息片段。 (文件来源: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);
          break;
      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)
          {
              QBrush redBackground(Qt::red);
              return redBackground;
          }
          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;
          }
      }
      return QVariant();
  }

每个格式化属性都将通过对data()方法的单独调用从模型中请求。角色参数用于让模型知道正在请求哪个属性:

enum Qt::ItemDataRoleMeaningType
Qt::DisplayRole文本Qt::DisplayRole
Qt::FontRole字体QFont
BackgroundRole用于单元格背景的画笔用于单元格背景的画笔
Qt::TextAlignmentRole文本对齐方式enum Qt::AlignmentFlag
Qt::CheckStateRole使用qvariant()取消复选框,或者使用qt::checked和Qt::Uncheckedenum Qt::ItemDataRole

请参阅qt命名空间文档以了解有关qt::itemDataRole枚举功能的更多信息。
  现在,我们需要确定使用分离的模型如何影响应用程序的性能,所以让我们跟踪视图调用data()方法的频率。为了跟踪视图调用模型的频率,我们在data()方法中放置了一个debug语句,该语句将记录到错误输出流中。在我们的小示例中,data()将被调用42次。每次将光标悬停在字段上时,都将再次调用data(),每个单元格调用7次。这就是为什么在调用data()并缓存昂贵的查找操作时,确保数据可用非常重要。

2.3 表单元内嵌入时钟

我们仍然有一个只读表,但这次内容会每秒更改一次,因为我们显示的是当前时间。 (文件来源:examples/widgets/tutorials/modelview/3 ngmodel/mymodel.cpp)
  QVariant MyModel::data(const QModelIndex &index, int role) const
  {
      int row = index.row();
      int col = index.column();

      if (role == Qt::DisplayRole)
      {
          if (row == 0 && col == 0)
          {
              return QTime::currentTime().toString();
          }
      }
      return QVariant();
  }

没有什么东西可以让时钟滴答作响。我们需要每秒钟告诉视图时间已经改变,需要重新阅读。我们用计时器来做这个。在构造函数中,我们将其间隔设置为1秒,并连接其超时信号。
(文件来源:examples/widgets/tutorials/modelview/3 ngmodel/mymodel.cpp)

  MyModel::MyModel(QObject *parent)
      :QAbstractTableModel(parent)
  {
  //    selectedCell = 0;
      timer = new QTimer(this);
      timer->setInterval(1000);
      connect(timer, SIGNAL(timeout()) , this, SLOT(timerHit()));
      timer->start();
  }

响应槽函数:
(文件来源: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

  void MyModel::timerHit()
  {
      //we identify the top left cell
      QModelIndex topLeft = createIndex(0,0);
      //emit a signal to make the view reread identified data
      emit dataChanged(topLeft, topLeft);
  }

我们要求视图通过发出dataChanged()信号再次读取左上角单元格中的数据。注意,我们没有显式地将dataChanged()信号连接到视图。这在我们调用setModel()时自动发生。

2.4 设置列和行的标题

可以通过该方法隐藏标题:tableView->verticalHeader()->hide();
在这里插入图片描述
但是,头部内容是通过模型设置的,因此我们重新实现headerdata()方法:
(文件来源:examples/widgets/tutorials/modelview/4_headers/mymodel.cpp)

  QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
  {
      if (role == Qt::DisplayRole)
      {
          if (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.h)

  // mymodel.h
  #include <QAbstractTableModel>
  #include <QString>

  const int COLS= 3;
  const int ROWS= 2;

  class MyModel : public QAbstractTableModel
  {
      Q_OBJECT
  public:
      MyModel(QObject *parent);
      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];  //holds text entered into QTableView
  signals:
      void editCompleted(const QString &);
  };

  我们使用二维数组qstring m_griddata来存储数据。这使得m_griddata成为mymodel的核心。MyModel的其余部分就像一个包装器,将m_GridData适配到QabstratemModel接口。我们还引入了editCompleted()信号,这使得将修改后的文本传输到窗口标题成为可能。
(文件来源:examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

  bool MyModel::setData(const QModelIndex & index, const QVariant & value, int role)
  {
      if (role == Qt::EditRole)
      {
          //save value from editor to member m_gridData
          m_gridData[index.row()][index.column()] = value.toString();
          //for presentation purposes only: build and emit a joined string
          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;
  }

  每次用户编辑单元格时都会调用setdata()。index参数告诉我们哪个字段已被编辑,value提供编辑过程的结果。角色将始终设置为qt::editRole,因为我们的单元格只包含文本。如果存在复选框,并且用户权限设置为允许选中该复选框,则还将使用设置为qt::checkStateRole的角色进行调用。
(文件来源:示例/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

  您可以将上面的示例转换为具有树视图的应用程序。只需将QTableView替换为QTreeView,这将导致读/写树。不必更改模型。树不会有任何层次结构,因为模型本身没有任何层次结构。
在这里插入图片描述
QListView, QTableView和QTreeView都使用le 一个包含列表、表和树的抽象模型。这使得可以使用同一模型中的几种不同类型的视图类。
在这里插入图片描述
到目前为止,我们的示例模型是这样的:
在这里插入图片描述
我们要展示一棵真正的树。为了建立一个模型,我们将数据包装在上面的示例中。这一次我们使用QStandardItemModel,它是一个层次数据容器,也实现了QAbstractItemModel。若要显示树,QStandardItemModel必须填充 QStandardItems,它能够保存文本、字体、复选框或画笔等项的所有标准属性。
在这里插入图片描述
(文件来源:examples/widgets/tutorials/modelview/6_treeview/mainwindow.cpp)


  // modelview.cpp
  #include <QTreeView>
  #include <QStandardItemModel>
  #include <QStandardItem>
  #include "mainwindow.h"

  const int ROWS = 2;
  const int COLUMNS = 3;

  MainWindow::MainWindow(QWidget *parent)
      : QMainWindow(parent)
  {
      treeView = new QTreeView(this);
      setCentralWidget(treeView);
      standardModel = new QStandardItemModel ;

      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)
  {
      QList<QStandardItem *> rowItems;
      rowItems << new QStandardItem(first);
      rowItems << new QStandardItem(second);
      rowItems << new QStandardItem(third);
      return rowItems;
  }

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

3.2 使用所选内容

我们想要访问所选项目的内容,以便将其与层次结构级别一起输出到窗口标题中。
在这里插入图片描述
所以让我们创建几个项目:
(文件来源:examples / widgets / tutorials / modelview / 7_selections / mainwindow.cpp)

  #include <QTreeView>
  #include <QStandardItemModel>
  #include <QItemSelectionModel>
  #include "mainwindow.h"

  MainWindow::MainWindow(QWidget *parent)
      : QMainWindow(parent)
  {
      treeView = new QTreeView(this);
      setCentralWidget(treeView);
      standardModel = new QStandardItemModel ;
      QStandardItem *rootNode = standardModel->invisibleRootItem();

      //defining a couple of items
      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");

      //building up the hierarchy
      rootNode->    appendRow(americaItem);
      rootNode->    appendRow(europeItem);
      americaItem-> appendRow(mexicoItem);
      americaItem-> appendRow(usaItem);
      usaItem->     appendRow(bostonItem);
      europeItem->  appendRow(italyItem);
      italyItem->   appendRow(romeItem);
      italyItem->   appendRow(veronaItem);

      //register the model
      treeView->setModel(standardModel);
      treeView->expandAll();

      //selection changes shall trigger a slot
      QItemSelectionModel *selectionModel= treeView->selectionModel();
      connect(selectionModel, SIGNAL(selectionChanged (const QItemSelection &, const QItemSelection &)),
              this, SLOT(selectionChangedSlot(const QItemSelection &, const QItemSelection &)));
  }

视图管理单独选择模型中的选项,可以使用selectionModel()方法检索。 我们检索选择模型,以便将槽函数连接到其selectionChanged()信号。
(文件来源:examples / widgets / tutorials / modelview / 7_selections / mainwindow.cpp)

  void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
  {
      //get the text of the selected item
      const QModelIndex index = treeView->selectionModel()->currentIndex();
      QString selectedText = index.data(Qt::DisplayRole).toString();
      //find out the hierarchy level of the selected item
      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()获得与选择相对应的模型索引,并通过使用模型索引获得字段的字符串。然后我们只计算项目的层次级别。顶级项没有父级,parent()方法将返回默认构造的QModelIndex()。这就是为什么我们使用parent()方法迭代到顶层,同时计算迭代期间执行的步骤。
  可以检索选择模型(如上所示),但也可以使用QAbstractItemView::setSelectionModel进行设置。因为只使用了一个选择模型的一个实例,所以有3个视图类可以进行同步选择。要在3个视图之间共享选择模型,请使用SelectionModel(),并使用SetSelectionModel()将结果分配给第二个和第三个视图类。

3.3 预定义的模型

使用Model/View的典型方法是包装特定数据,使其可用于视图类。然而,Qt还为常见的底层数据结构提供了预定义的模型。如果可用的数据结构之一适合您的应用程序,那么预定义的模型是一个不错的选择。

QStringListModel存储字符串列表
QStandardItemModel存储任意层次项目
QFileSystemModel、QDirModel封装本地文件系统
QSqlQueryModel封装SQL结果集
QSqlTableModel封装SQL表
QSqlRelationalTableModel封装带外键的SQL表
QSortFilterProxyModel排序和/或筛选其他模型

3.4 代理

  在目前为止的所有示例中,数据在单元格中显示为文本或复选框,并作为文本或复选框进行编辑。 提供这些演示和编辑服务的组件称为委托。 我们才刚刚开始使用委托,因为视图使用默认委托。 但是想象一下,我们想要一个不同的编辑器(例如,滑块或下拉列表)或想象我们想要将数据呈现为图形。 让我们来看一个名为Star Delegate的例子,其中星星用于显示评级:
在这里插入图片描述
视图有一个setItemDelegate()方法,该方法替换默认委托并安装自定义委托。可以通过创建继承自QStyledItemDelegate的类来编写新委托。为了编写一个显示星型并且没有输入功能的委托,我们只需要重写2个方法。

  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()方法用于获取每个星的尺寸,因此单元将提供足够的高度和宽度来容纳星。
  如果要在视图类的网格中使用自定义图形表示来显示数据,则编写自定义委托是正确的选择。如果您想离开网格,您将不会使用自定义委托,而是使用自定义视图类。
其他参考:

3.5 使用ModelTest进行调试

  模型的被动性为程序员提供了新的挑战。模型中的不一致可能导致应用程序崩溃。由于模型受到视图中无数调用的影响,很难找出哪个调用使应用程序崩溃,以及哪个操作引入了问题。
  QtLabs提供了一个名为ModelTest的软件,它在您的编程运行时检查模型。每次模型更改时,ModelTest都会扫描模型并使用断言报告错误。这对于树模型尤其重要,因为它们的层次结构特性会留下许多细微不一致的可能性。
  与视图类不同,ModelTest使用超出范围的索引来测试模型。这意味着您的应用程序可能会与ModelTest一起崩溃,即使没有它也能完美运行。因此,在使用ModelTest时,还需要处理超出范围的所有索引。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐