1. Basic Model/View Principles
GUI application development often involves the application of list boxes, tables, tree structures and other forms. Of course, Qt also provides the corresponding view classes QListView, QTableView, and QTreeView, which use the model/view Model/View architecture to manage the relationships between data and how they are presented to users. The functional separation introduced by this architecture provides developers greater flexibility to customize the representation of data items and provides a standard model interface that allows a wide range of data sources to be used in existing views.
Data: is the actual data, which can be a data table or SQL query result of a database, a StringList in memory, or a file, etc.
View:GUI interface component that gets a model index for each data line from the data model, captures data from the model thumbnail, and then provides display data for the interface component. Examples include QListView, QTableView, QTreeView, and so on.
Model: Communicates with actual data and provides data interfaces for view components. It can be understood as data adapter, data wrapper. It extracts what is needed from the original data for display and editing of view components.
The benefits of this design are:
- Model/View separates the data source from the display interface and decouples the code.
- You can also display the same data model in different views.
- You can also design special views without modifying the data model.
Delegate: In the model/view structure, there is also a Delegate function, which allows users to customize the interface display and editing of data.
Model, view, delegate communicate using signals and slots. When the data changes, the model signals the view;
When a user manipulates data on the UI (check, click, etc.), the view signals these operations;
When the user edits the data, delegate signals the state of the model and view editor.
1. Data Model
QAbstractItemModel is the base class for all data models, which defines the interfaces for view ing and delegate to access data. However, the original data does not have to be stored in the model.
Typically, we use QListView, QTableView, and QTreeView to generate our own custom data model classes, which inherit from QAbstractListModel, QAbstractTableModel, and QAbstractItemModel, respectively.
2. View Component View
View component View is an interface component that displays data from a data model. Qt provides the following common view components:
- QListView: Displays a single column of list data, suitable for one-dimensional data operations;
- QTreeView: Displays tree-structured data, suitable for manipulation of tree-structured data;
- QTableView: Displays tabular data, suitable for manipulation of two-dimensional tabular data.
The setModel() function of the view class completes the data binding of the view and model, and modifications on the view are automatically associated with the model.
3. Proxy delegate
A proxy is an editor that provides editing data on a view component, such as double-clicking a cell in the table component to edit data, and using the QLineEdit edit box by default. The role of the agent is to fetch data from the model, display it in the editor, modify the data, and save it in the model.
Custom proxy classes are usually created using a class that needs to be derived from the QStyledItemDelegate class.
2. Use of QAbstractTableModel
From the analysis above, we know that QAbstractTableModel, which mainly provides the data model interface for the QTableView, can subclass the abstract class and implement the related interface. Let's demo a simple 9*9 multiplication trick to see how it works:
There are three interfaces that must be implemented:
//Number of rows returned int rowCount(const QModelIndex &parent = QModelIndex()) const override; //Return number of columns int columnCount(const QModelIndex &parent = QModelIndex()) const override; //Return current data based on model index QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Of course, only the three interfaces that are required do not seem to work well. First, we need to give the model an initial value.
- Add setInitData member function, load data and refresh.
void MyTableModel::setInitData(QList<CellInfo*>& data) { //Call beginResetModel before resetting model data, which triggers the modelAboutToBeReset signal beginResetModel(); //Reset data in model m_datas = data; m_rowNum = ceil(data.size()*1.0/m_columnNum); //Rows = Total data / Columns, then rounded up //The endResetModel is called after the data has been set, which triggers the modelReset signal endResetModel(); }
- Returns rowCount(),columnCount(), because there are 9 rows and columns, so it returns a fixed 9
int MyTableModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } else { return m_rowNum; } } int MyTableModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } else { return m_columnNum; } }
- Dynamic addition and deletion of rows and columns
Because the rows and columns of this 9*9 multiplication trick table are fixed, there is no need to operate rows and columns at this time. The related interfaces are as follows, which can be added as needed.
//Insert Related Interface bool insertColumn(int column, const QModelIndex &parent = QModelIndex()) virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) bool insertRow(int row, const QModelIndex &parent = QModelIndex()) virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) //Remove related interfaces bool removeColumn(int column, const QModelIndex &parent = QModelIndex()) virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) bool removeRow(int row, const QModelIndex &parent = QModelIndex()) virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex())
- The most important interface data() function
view can provide different data in different dimensions by rendering its own data through the data() function provided by the model.
QVariant MyTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if(index.row()*m_columnNum+index.column() < m_datas.count()) { if (role == Qt::DisplayRole || role == Qt::EditRole) { return m_datas[index.row()*m_columnNum+index.column()]->content;//Presentation of data } else if(role == Qt::BackgroundColorRole){ return m_datas[index.row()*m_columnNum+index.column()]->bgColor;//Cell Background Color } else if (role == Qt::TextAlignmentRole) { //On its way return Qt::AlignCenter; } else if(role == Qt::ToolTipRole){ return m_datas[index.row()*m_columnNum+index.column()]->toolTip;//Prompt for data } else if(role == Qt::UserRole) { return QVariant::fromValue(m_datas[index.row()*m_columnNum+index.column()]); } } return QVariant(); }
You can see the enumeration of the ItemDataRole, and you want to see the dimension in which the data is displayed by writing into else if.
enum ItemDataRole { DisplayRole = 0, DecorationRole = 1, EditRole = 2, ToolTipRole = 3, StatusTipRole = 4, WhatsThisRole = 5, // Metadata FontRole = 6, TextAlignmentRole = 7, BackgroundRole = 8, ForegroundRole = 9, #if QT_DEPRECATED_SINCE(5, 13) // ### Qt 6: remove me BackgroundColorRole Q_DECL_ENUMERATOR_DEPRECATED = BackgroundRole, TextColorRole Q_DECL_ENUMERATOR_DEPRECATED = ForegroundRole, #endif CheckStateRole = 10, // Accessibility AccessibleTextRole = 11, AccessibleDescriptionRole = 12, // More general purpose SizeHintRole = 13, InitialSortOrderRole = 14, // Internal UiLib roles. Start worrying when public roles go that high. DisplayPropertyRole = 27, DecorationPropertyRole = 28, ToolTipPropertyRole = 29, StatusTipPropertyRole = 30, WhatsThisPropertyRole = 31, // Reserved UserRole = 0x0100 };
- By default, double-clicking is not editable, and member functions flags(),setData(), can be overridden to allow editing, and edit box values can be updated to the model and notified of view.
Qt::ItemFlags MyTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsEditable; } bool MyTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { if(index.row()*m_columnNum+index.column() < m_datas.count()) { if (index.isValid() && role == Qt::EditRole) { m_datas[index.row()*m_columnNum+index.column()]->content = value.value<QString>(); emit dataChanged(index, index, QVector<int>() << role); //Send signal to trigger refresh return true; } if (index.isValid() && role == Qt::BackgroundColorRole) { m_datas[index.row()*m_columnNum+index.column()]->bgColor = value.value<QColor>(); emit dataChanged(index, index, QVector<int>() << role); //Send signal to trigger refresh return true; } } return false; }
- Using a delegate
If tableview does not specify delegate, the default is to use the QLineEdit edit box. From the previous introduction, we know that delegate acts as a bridge between model s and views to transfer data to and from one another.
I want to double-click to edit the cell color, as shown below. What should I do?
- First subclassify QStyledItemDelegate, in order to implement the base class of four virtual functions, the role of four functions, the notes should be clear. delegate is a bridge and a middleman, so model-view should be arranged on both sides.
class MyColorSelDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit MyColorSelDelegate(QObject *parent = nullptr); ~MyColorSelDelegate(); //Create widget components for editing model data, such as a QSpinBox component or a QComboBox component; QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; //Get data from the data model for editing by the widget component; void setEditorData(QWidget *editor, const QModelIndex &index) const override; //Update the data on the widget to the data model; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; //Used to set an appropriate size for widget components; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; signals: };
- Next, createrEditor(), returns the QWidget object pointer of the QColorDialog color pickup box that comes with the system, which can also be any widget we customize here.
- Then setEditorData(), which gets data from the data model for editing by the widget component;
- Finally setModelData() updates the data on the widget to the data model;
QWidget *MyColorSelDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QWidget* editor = new QColorDialog(parent); return editor; } void MyColorSelDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { } void MyColorSelDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QColorDialog* dlg = static_cast<QColorDialog*>(editor); model->setData(index, dlg->selectedColor(), Qt::BackgroundColorRole); } void MyColorSelDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { editor->setGeometry(option.rect); }
3. Last
Through the actual operation of QAbstractTableModel and QTableView, we should be able to understand how model/view works.
Complete code has been uploaded to the code cloud
Official documents: https://doc.qt.io/qt-5/qabstracttablemodel.html
Official documents: https://doc.qt.io/qt-5/model-view-programming.html