Qt literacy-Model/View entry

I. Overview

Every UI developer should know about ModelView programming,

Table control, list control and tree control are frequently used components in gui. These controls have two different ways to access their data.

  • 1. Traditional method
    The traditional method is to let the control itself store data, and there is a data container inside the control. This method is very intuitive, but it will cause data synchronization problems in many important applications.
  • 2.Model/View method
    Model/View is also called model/view programming, in which the control does not maintain an internal data container. They access external data through standardized interfaces, thus avoiding data duplication. At first glance, this may seem complicated, but once you take a closer look, not only is it easy to grasp, but the many benefits of Model/View programming become clearer.
    insert image description here

Along the way, we'll learn some of the basic technologies Qt provides, such as:

  • Difference between standard widgets and Model/View widgets
  • Adapter between form and model
  • Develop simple Model/View applications
  • pre-defined model
  • Intermediate topics, such as:
    tree view
    selection
    agent
  • model test debugging

We'll also see if new applications can be written more easily using Model/View programming, or if traditional controls will work just as well.

2. Introduction

Model/View is a technique for separating data and view in controls that handle datasets. Standard controls are not designed to separate data from the view, which is why Qt has two different types of controls. Although the two types of controls look the same, they interact with data differently.

The data used by standard controls is part of the control. Data and interface are integrated
insert image description here
view class to operate external data (model), which is to classify interface and data through interface, model stores data, and view is a view class to display UI.
insert image description here

1. Standard parts

Let's take a closer look at a standard table control. A table control is a 2D array of data elements that the user can change. The grid control can be integrated into the program flow by reading and writing the data elements provided by the grid control. This approach is very intuitive and useful in many applications, but displaying and editing database tables using standard table controls can be problematic.

What's the problem?

That is we have to coordinate two copies of the data: one outside the control; one inside the control . The developer is responsible for synchronizing the two versions. Manual synchronization is prone to risks.

Besides that, the tight coupling of table representation and data makes it harder to write unit tests. It's not convenient to test!

2. Model/View control

Model/View gradually provides a solution using a more general architecture. Model/View eliminates data consistency problems that can arise with standard controls.

Model/View also makes it easier to work with multiple views of the same data, since a model can be passed to multiple views.

The most important difference is that Model/View controls do not store data behind table cells. In fact, they operate directly on your data. Since the view class does not know the structure of the data, it needs to provide a wrapper to make the data conform to the QAbstractItemModel interface. The view uses this interface to read and write data.

Any instance of a class implementing QAbstractItemModel is called a model. Once the view receives a model pointer, it reads and displays its content, and becomes its editor.

3. Model/View control overview

Below is an overview of the Model/View controls and their corresponding standard controls.

control appearance Standard widgets (Item-based convenience classes) Model/View view classes (for external data)
insert image description here QListWidget QListView
insert image description here QTableWidget QTableView
insert image description here QTreeWidget QTreeView
insert image description here Rarely used QColumnView displays the tree as a hierarchy of columns
insert image description here QComboBox can be used as a view class or as a traditional control

4. Use Adapters between the form and the model

Using adapters between forms and models can come in handy.

We can edit the data stored in the table directly from the table itself, but it is much more comfortable to edit the data in the text field. For controls that operate on a value (QLineEdit, QCheckBox…) rather than a dataset, there is no direct Model/View counterpart to separate the data and the view, so we need an adapter to connect the form to the data source.

QDataWidgetMapper is a great solution because it maps form widgets to table rows and makes it very easy to build form forms for database tables. This is equivalent to using QDataWidgetMapper to synchronize the modified data into the table through the model/view.
insert image description here

Another example of an adapter is QCompleter. Qt has QCompleter for providing autocompletion in Qt controls like QComboBox and QLineEdit as shown below. QCompleter uses a model as its data source.
insert image description here
Just like the following simple code can achieve this effect

QStringList wordList;
wordList << "alpha" << "omega" << "omicron" << "zeta"<<"Emanuel"<<"Emerson"<<"Emilio"<<"Emma"<<"Emmily";
QLineEdit *lineEdit = new QLineEdit(this);

QCompleter *completer = new QCompleter(wordList, this);
completer->setCaseSensitivity(Qt::CaseInsensitive);
lineEdit->setCompleter(completer);

3. Simple model / view application example

If you want to develop a model / view application, where should you start? The official Qt recommendation is to start with a simple example and gradually extend it. This makes understanding the architecture much easier. Trying to understand the model/view architecture in detail before calling the IDE is difficult for many developers. But it's easier in practice from a simple model/view application.

Below are 7 very simple and self-contained applications that demonstrate different aspects of model/view programming

1. A read-only table

We start with an application that uses a QTableView to display data. We'll add editing functionality later.

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

We have the usual main() function:
we create an instance of MyModel and
pass its pointer to the tableView using tableView.setModel(&myModel);

tableView calls methods on the pointer it receives to find out two things:

  • How many rows and how many columns should be displayed.
  • What each cell should print.

model needs some code to respond to this request. We have a table dataset, so let's start with QAbstractTableModel since it is easier to use than the more general qabstractemmodel.

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

qabstractablemodel needs to implement three abstract methods.

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

The number of rows and columns is provided by MyModel::rowCount() and MyModel::columnCount(). When the view needs to know what the cell's text is, it calls the method MyModel::data(). The row and column information is specified by the index parameter, and the role is set to Qt::DisplayRole. Other roles are covered in the next section.

In our case, the data that should be displayed has already been generated. In a real application, MyModel would have a member named MyData which acts as the target for all read and write operations.

This small example demonstrates the passive nature of the model. ** The model doesn't know when it will be used or what data it needs. It just provides data every time the view is requested. **

What happens when the model's data needs to change? How does the view realize that the data has changed and needs to be read again? model must emit a signal indicating which ranges of cells changed. This will be demonstrated in Section 3.

2. Use role to extend the read-only example

In addition to controlling the text displayed by the view, the model also controls the appearance of the text. After slightly modifying the model, the following results are obtained:
insert image description here

Actually, we don't need to modify anything except the data() method to set the font, background color, alignment and checkbox. Below is the data() method that produces the result shown above. The difference is that this time we use the formal parameter int role to return different information according to its value.

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

Each formatting attribute is requested from the model by calling the data() method individually. The role parameter is used to let the model know which attribute is requested:

enum Qt::ItemDataRole Meaning Type
Qt::DisplayRole text QString
Qt::FontRole font QFont
BackgroundRole Cell background brush QBrush
Qt::TextAlignmentRole text alignment enum Qt::AlignmentFlag
Qt::CheckStateRole 支持CheckBox with QVariant(), sets checkboxes with Qt::Checked or Qt::Unchecked enum Qt::ItemDataRole
Qt::DecorationRole Decorative data presented as icons QColor, QIcon or QPixmap

Qt::ForegroundRole | Foreground brush (usually text color | QBrush) for items rendered with default delegates

Please refer to the Qt namespace documentation for more information on the functionality of the Qt::ItemDataRole enumeration.

Now we need to determine how using a detached model affects the performance of the application, so let's track how often the view calls the data() method. To track how often the view calls the model, we add debug statements in the data() method, which are logged to the error output stream.

In the small example above, data() will be called 42 times. Every time the cursor is hovered over the field, data() will be called again - 7 times per cell.

The statements under the case corresponding to the above 7 Roles are respectively executed.

That's why it's important to make sure the data is available when calling data() and caching expensive lookup operations .

3. Clocks in table cells

insert image description here

We still have a read-only table, but this time the content changes every second because we're displaying the current time.

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

There are fewer things to keep the clock ticking. We need to tell the view every second that the time has changed and needs to be re-read. We do this with a timer. In the constructor we set its interval to 1 second and connect its timeout signal.

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

Here are the corresponding slots:

  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, {
    
    Qt::DisplayRole});
  }

We call the dataChanged() function to tell the view to read the data in the top left cell again. Note that we did not explicitly connect the dataChanged() signal to the view. This happens automatically when we call setModel().

4. Set headers for columns and rows

Title can be hidden by view method
insert image description here

tableView->verticalHeader()->hide();

However, the header content is set via the model, so we reimplement the headerData() method:

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

Note that the headerData() method also has a role, which has the same meaning as the role in MyModel::data().

5. Minimal edited example

In this example, we'll build an application that automatically populates window titles with content by repeating values ​​entered in table cells. In order to have easy access to the window title, we put the QTableView inside the QMainWindow.

The model determines whether editing functionality is provided. We just need to modify the model so that the available editing features are enabled. This is achieved by reimplementing the following virtual methods: setData() and flags().

  // 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];  //holds text entered into QTableView
  signals:
      void editCompleted(const QString &);
  };

We use two-dimensional array QString m_gridData to store data. This makes m_gridData the heart of MyModel. The rest of MyModel acts like a wrapper and adapts m_gridData to the QAbstractItemModel interface. We also introduced the editCompleted() signal, which transfers the modified text into the window title.

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

Whenever the user edits a cell, setData() is called. The index parameter tells us which field was edited, and the value parameter provides the result of the editing process. Role will always be set to Qt::EditRole because the cell contains only text. If a checkbox exists, and the user permissions are set to allow checking the checkbox, the call also sets the role to Qt::CheckStateRole.

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

You can use flags() to adjust various properties of the cell.

Returning Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled is sufficient to show that the editor can select a cell.

If editing a cell modifies more data than is in that particular cell, the model must emit the dataChanged() signal in order to read the changed data. Just like the writing method in 3, manually emit the dataChanged() signal by yourself

4. Topics in the middle

1. TreeView

You can convert the above example into an application with a tree view. Just replace QTableView with QTreeView and you get a read/write tree. No changes to the model are required. The tree won't have any hierarchy because the model itself doesn't have any hierarchy.
insert image description here
QListView, QTableView and QTreeView all use a model abstraction which is a combined list, table and tree. This makes it possible to use multiple view classes of different types in the same model.

insert image description here

Our example model looks like this so far:
insert image description here
we want to render a real tree. To build the model, we wrap the data in the example above. This time we use QStandardItemModel, which is a container for hierarchical data and also implements QAbstractItemModel. In order to display the tree, the QStandardItemModel must be filled with QStandardItems, which are able to hold all the standard properties of the item, such as text, font, checkbox or brush.
insert image description here

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

We just instantiate a QStandardItemModel and add some QStandardItems in the constructor. We can then build a hierarchical data structure, since a QStandardItem can hold other QStandardItems. Nodes are collapsed and expanded in the view.

2. Using selectors

We want to access the content of the selected item so that it is output in the window title along with the hierarchy.
insert image description here

Let's create a few entries:

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

      //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, &QItemSelectionModel::selectionChanged,
              this, &MainWindow::selectionChangedSlot);
  }

Views manage selections in a single selection model, which can be obtained with the selectionModel() method. We get the selection model in order to connect the slot to its selectionChanged() signal.

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

We get the model index corresponding to the selection by calling treeView->selectionModel()->currentIndex(), and get the string of the field through the model index. Then we just need to calculate the hierarchy of items. The top-level element has no parent element, the parent() method will return a default constructed QModelIndex().

That's why we use the parent() method to iterate to the top level while counting the number of steps performed during the iteration.

The selection model (shown above) can be retrieved, but it can also be set with QAbstractItemView::setSelectionModel. That's why it is possible to have 3 view classes selecting simultaneously, because only one instance of the selection model is used. To share a selection model between 3 views, use selectionModel(), and assign the result to the second and third view class with setSelectionModel().

3. Predefined models

The typical way to use model/view is to wrap specific data and make it available in the view class. However, Qt also provides predefined models for common underlying data structures. If one of the data structures available is suitable for your application, the predefined models might be a good choice.

model name meaning
QStringListModel store a list of strings
QStandardItemModel store arbitrary hierarchical items
QFileSystemModel Encapsulated local file system
QSqlQueryModel Encapsulate a SQL result set
QSqlTableModel Encapsulate a SQL table
QSqlRelationalTableModel Encapsulate an SQL table with foreign keys
QSortFilterProxyModel Sort and/or filter another model

4. Delegates

In all the examples so far, the data is present in cells as text or checkboxes and editable as text or checkboxes. Components that provide these presentation and editing services are called Delegates. We've only just started using Delegates because the view uses the default delegates. However, suppose we want to have a different editor (for example, a slider or a drop-down list), or want to display the data in the form of a graph. Let's look at an example called Star Delegate, which uses asterisks to represent ratings:
insert image description here

This view has a setItemDelegate() method that replaces the default delegates and installs a custom one. New Delegates can be written by creating a class that inherits from QStyledItemDelegate. In order to write a Delegates that displays asterisks and has no input functionality, we only need to override two methods.

  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() draws asterisks based on the contents of the underlying data. Data can be looked up by calling index.data().

The sizeHint() method of the Delegates is used to get the size of each asterisk so that the cell provides enough height and width to accommodate the asterisks.

Writing custom delegates is the right choice if you want to display data using a custom graphical representation in a grid of view classes. If you want to go away from the grid, you don't use custom Delegates, but custom view classes.

Other references to Delegates in the Qt documentation:

Spin BoxDelegates Example
QAbstractItemDelegate Class Reference
QSqlRelationalDelegate Class Reference
QStyledItemDelegate Class Reference
QItemDelegate Class Reference

Guess you like

Origin blog.csdn.net/qq_43680827/article/details/132133137