Model-View-Delegate Pattern in Qt

The Qt’s Model-View-Delegate Pattern
The Qt’s Model-View-Delegate Pattern

Qt provides out of the box support for model-view-delegate programming. Many of our day-to-day programming problems can fit very well into this pattern as well. In this article I will explain some basic ideas about how to build a simple model-view based application.

The Problem

You have to develop a simple software for calculating checksum. The basic requirements are listed below.

  1. A table should show a bunch of errors.
  2. The first column in the table should show a human-readable error text.
  3. The second column should have an integer code, for all error texts.
  4. A combobox should show all the available errors in the table.
  5. The combobox should only show human-readable error text.
  6. A text box, which will accept a string from the user.
  7. A push button, which will take the message and the error code, and will calculate modulo-256 checksum.
  8. This checksum should be displayed on the GUI.
  9. The error table should provide action-buttons, to add, delete items from and to the table.
  10. The table content should be editable by double click.
  11. When the user double clicks on the error code, the GUI should show a slider with a range of [0-255] to select the error code value.

Understanding the problem in detail

Basically, there is some data, a list of errors. Each error is composed of two parts. A human readable error text, and an integer error code. This data should be displayed by two different GUI widgets at the same time. Namely a table as well as a combobox. While the table can show the data fully, the combobox only can show the error text. The advantage of combobox over a table is that, the selection is explicitly made by the user. And finally, when you click on a button, get the selected data, and do some basic computation to find checksum. In case, if the user decides to edit the error codes, they should be able to do it, using a slider control.

Step-1 : Building a Model

I have built a table with a couple of random error texts and code that goes into our model.

ERROR TEXT ERROR CODE
No Error 0
Data Fault 15
Unknown Input 35
Unsupported Baud Rate 44
Reset Counter 70
Control Port Inactive 189

Qt supports a bunch of classes with which you can easily build your models. For this problem, I have chosen QStandardItemModel class. The process of setting your data into a model is pretty much manual.

// Your data can come from anywhere.
// A file, database, network.
// I'm hard-coding for this example.
QStringList error_texts;
QList<int> error_codes;
error_texts << "No Error"
            << "Data Fault"
            << "Unknown Input"
            << "Unsupported Baud Rate"
            << "Reset Counter"
            << "Control Port Inactive";
    
error_codes << 0
            << 15
            << 35
            << 44
            << 70
            << 189;

// Now, Build your Model and load the data
model = new QStandardItemModel(error_codes.size(), 2, this);
for(int i = 0, n = error_codes.size(); i < n; i++)
{
    model->setData(model->index(i, 0), error_texts.at(i));
    model->setData(model->index(i, 1), error_codes.at(i));
}

Now you have your model ready. Now it is time to build the GUI for the application. Many Qt widgets supports the setModel() method out of the box.

Note : that Qt has both item-based and model-based widgets in the toolbox. Since we are planning our project to be model-view based, always choose Item-Views (Model-Based) instead of Item-Widgets (Item-Based).

A simple gui with model based views
A simple gui with model based views

Set the model to appropriate views on the GUI.

// Set the model to our widgets.
ui->tbl_error->setModel(model);
ui->cmbx_error->setModel(model);
With not much of programming, the widgets are loaded and ready.
With not much of programming, the widgets are loaded and ready.

Note that both the combo box as well as the table shares the same data. Combo box shows, only the first column in the model. But combobox’s current selection index can be used to navigate through the model, to find the error code we need.

Step-2 : Add/Delete Actions on the Model

Lets quickly implement the Add and Delete options provided to the model. This is pretty straight forward. As soon as you modify the model, all the associated views gets updated with the new data. Define slots for clicks on both the Add and Delete buttons.

void Dialog::on_btn_delete_clicked()
{
    QItemSelectionModel* selection = ui->tbl_error->selectionModel();
    if(selection->hasSelection())
    {
        if(!selection->selectedRows().isEmpty())
        {
            QModelIndexList indexes = selection->selectedRows();
            for(int i = 0, n = indexes.count(); i < n; i++)
            {
                model->removeRow(indexes.at(i).row());
            }
        }
    }
}

void Dialog::on_btn_add_clicked()
{
    model->insertRow(model->rowCount());
}

That was quick. With this much code, you should be able to select rows from the table, and delete them, and add rows to the bottom of the table. Interesting fact is that, as soon as the table data is updated, the combobox is also updated. Qt’s Model-View system uses internal signals and slots to update all connected items.

Note: that you can double click on any table cell and edit the data. This behaviour is provided by the default delegate used by the QTableView. For integers, a spin box is provided by the default delegate. This behaviour can be changed using custom Delegates.

Step-3 : Introducing a Delegate

A delegate is some class, to which you offload some work, which is to be done on your behalf. In case of model-view-delegate pattern, the delegate decides how the model is presented on the view, and how the updates from the view gets transported back to the model. Qt provides a number of classes which might suit your specific delegation problem. For this example, I’m using QItemDelegate. QItemDelegate is an abstract class, and there a bunch of mandatory overrides.

VIRTUAL FUNCTION SIGNATURE WHAT DOES IT DO?
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; When you want to edit an item in a model, this function decides what type of editor widget is provided to you.
void setEditorData(QWidget * editor, const QModelIndex & index) const; When you enter into edit mode, this function will set initial values to the editor widget provided by createEditor() override. This value ideally comes from the model.
void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const; When you are about to exit edit mode, this function will set the updated value from the editor widget back to the model.
void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const; This function will decide, how the editor widget is presented in a view.

As per our requirement, we need to provide a slider widget, instead of the default editor, when the user try to edit the error-codes in the error table. Lets implement our custom delegate keeping this in mind.

/*** mdelegate.h ***/

#ifndef MDELEGATE_H
#define MDELEGATE_H

#include <QWidget>
#include <QSlider>
#include <QAbstractItemModel>
#include <QStyleOptionViewItem>
#include <QItemDelegate>
#include <QLineEdit>

class MDelegate : public QItemDelegate
{
public:
    MDelegate();
    QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
    void setEditorData(QWidget * editor, const QModelIndex & index) const;
    void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const;
    void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const;
};

#endif // MDELEGATE_H

/*** mdelegate.cpp ***/

#include "mdelegate.h"

MDelegate::MDelegate()
{

}

QWidget* MDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    switch (index.column()) {
    case 0:
        return new QLineEdit(parent);
    case 1:
        QSlider* slider = new QSlider(parent);
        slider->setOrientation(Qt::Horizontal);
        slider->setAutoFillBackground(true);
        slider->setMaximum(255);
        return slider;
    }
}

void MDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
{
    switch (index.column()) {
    case 0:
    {
        QString text = index.model()->data(index, Qt::EditRole).toString();
        QLineEdit* line = static_cast<QLineEdit*>(editor);
        line->setText(text);
        break;
    }
    case 1:
    {
        int value = index.model()->data(index, Qt::EditRole).toInt();
        QSlider* slider = static_cast<QSlider*>(editor);
        slider->setValue(value);
        break;
    }
    }
}

void MDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
{
    switch (index.column()) {
    case 0:
    {
        QLineEdit* line = static_cast<QLineEdit*>(editor);
        model->setData(index, line->text(), Qt::EditRole);
        break;
    }
    case 1:
    {
        QSlider *slider = static_cast<QSlider*>(editor);
        model->setData(index, slider->value(), Qt::EditRole);
        break;
    }
    }
}

void MDelegate::updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
    editor->setGeometry(option.rect);
}

Although this is a bit of code, it is pretty straight forward. If the editing happens in the first column, we just deal with a simple QLineEdit. If it is the second column, where the error codes are located, we use a QSlider. Lets see how well it works.

The slider nicely fits into the cell geometry
The slider nicely fits into the cell geometry

Now, our model-view-delegate work is completely done. The only thing now left is calculating our checksum when the user clicks on the Build button. Although it is pretty irrelevant how this is done to this project, lets add that code as well.

void Dialog::on_btn_build_clicked()
{
    QByteArray bytes = ui->txt_msg->text().toLocal8Bit();
    int index = ui->cmbx_error->currentIndex();
    int error_code = model->data(model->index(index, 1)).toInt();
    bytes.append(error_code);

    uint8_t sum = 0;
    for(int i = 0; i < bytes.count(); i++)
    {
        sum += bytes.at(i);
    }
    ui->txt_checksum->setText(QString("Modulo 256 Checksum = %1").arg(sum));
}

Hope you have enjoyed this article. To learn more about Model/View programming pattern using Qt, I recommend reading the official documentation as well. Here is the link to the same.