Qt Interview Framework

An interesting topic in Qt is the Interview Framework. Interview is Qt’s approach to the model view concept. That said, Qt’s implementation isn’t really the easiest in the world to use. One of my least favorite bits about it is on the view end: delegates. To demonstrate this, in this blog we will have a look at an example, that we will refine step by step.

Interview, the model/view architecture

In short, the interview framework consists of the following components:

Overview

The model communicates with a source of data, providing an interface for the other components in the architecture. The nature of the communication depends on the type of the data source, and the way the model is implemented.
The view obtains model indexes from the model; these are references to items of data. By supplying model indexes to the model, the view can retrieve items of data from the data source and display it.

In standard views, a delegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.

To get a better understanding of what this means we should have a look at an example. So let’s play a bit with those concepts.

Simple Example

(The following example was developed with Qt 5.01 and run under Windows and Linux.)

First we will start with a very basic example without any custom-made delegate.

Our model (ListModel) inherits from QAbstractItemModel and contains a list of ListItem elements. The ListItem element encapsulates the attributes of a chess figure. It contains a figure name, a party name and an Image of the figure.

Collection

The ListItem class looks like this: (Note: The implementation is in the header file for a better readability)

struct ListItem
{
    ListItem(QString figureName, QString partyName, QString image):
        mFigureName(figureName),
        mPartyName(partyName),
        mImage(image)
    {}

    QString mFigureName;
    QString mPartyName;
    QPixmap mImage;
};

And the ListModel class looks like this:

class ListModel :
    public QAbstractItemModel
{
    Q_OBJECT

    private:
    QList<ListItem*> mListItem;

    public:
    explicit ListModel(QObject* parent = 0):
        QAbstractItemModel(parent)
    {
        // Add chess figures
        ListItem* king = new ListItem("King", "White", ":/images/King.png");
        mListItem.append(king);

        ListItem* queen = new ListItem("Queen", "White", ":/images/Queen.png");
        mListItem.append(queen);

        ListItem* bishop = new ListItem("Bishop", "White", ":/images/Bishop.png");
        mListItem.append(bishop);

        ListItem* knight = new ListItem("Knight", "White", ":/images/Knight.png");
        mListItem.append(knight);

        ListItem* rook = new ListItem("Rook", "White", ":/images/Rook.png");
        mListItem.append(rook);

        ListItem* pawn = new ListItem("Pawn", "White", ":/images/Pawn.png");
        mListItem.append(pawn);
    }

    // return data as variant. Role selects image or text
    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const
    {
        ListItem* item = mListItem.at(index.row());
        if (!item) return QVariant();

        switch(role)
        {
            // qt specific roles
            case  Qt::DisplayRole:
                return item->mFigureName;
            break;

            case Qt::DecorationRole:
                return item->mImage;
            break;

            default:
                return QVariant();
        }
    }

    // return empty index for flat structures. (special behavior needed for tree views)
    virtual QModelIndex parent(const QModelIndex& index) const
    {
        Q_UNUSED(index);
        return QModelIndex();
    }

    // create and return a standard index. (special behavior needed for tree views)
    virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const
    {
        Q_UNUSED(parent);
        return createIndex(row, column);
    }

    // return row count. (here size of the list)
    virtual int rowCount(const QModelIndex& parent) const
    {
        Q_UNUSED(parent);
        return mListItem.size();
    }

    // return columns. (here only 1)
    virtual int columnCount(const QModelIndex& index) const
    {
        Q_UNUSED(index);
        return 1;
    }
};

The “data” method returns some Attributes belonging to the indexed item. At the moment, it returns only the figure name (the Display Role) and the Image (the Decoration Role). These are the predefined standard roles of Qt to pass data to the user of the model (We will see below, how this can be extended to support custom roles).

The method “parent” returns an empty index (The item has no parent, since we have a flat model). The method index creates a QModelIndex based on the row and column attributes. The implementation of the methods depend on the underlying model and might get more complicated, if your model is based on a different structure, such as a tree structure.

For the view we can use a default implementation of Qt, the QListView. All we have to do now is to put everything together (Here we use a QmainWindowClass for simplicity.).

MainWindow::MainWindow(QWidget* parent) :
    QMainWindow(parent)
{
    QListView* listView = new QListView(parent); // create the view
    setCentralWidget(listView); // set the view as central widget

    // Layout
    listView->setViewMode(QListView::IconMode); // set mode
    listView->setFlow (QListView::LeftToRight); // left to right
    listView->setWrapping(false); // no wrapping
    listView->setModel(new ListModel(parent)); // create and set the model
}

The result looks like this.

Picture1

Custom look

Next we want to customize the look. As you can see the party name of the item is not displayed. We would like to add it on a second line below the figure name.
First of all we have to extend the model to support an additional role. We have to define a new one and support it in the data method of the ListModel.

class ListModel :
    public QAbstractItemModel
{
...
    public:
    enum DataRole
    {
        DR_PARTYNAME = Qt::UserRole + 100 // custom data role
    };
...
    // return data as variant. Role selects image or text
    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const
    {
        ListItem* item = mListItem.at(index.row());
        if (!item) return QVariant();

        switch(role)
        {
            // qt specific roles
            case  Qt::DisplayRole:
                return item->mFigureName;
            break;

            case Qt::DecorationRole:
                return item->mImage;
            break;

            case DR_PARTYNAME:
                return item->mPartyName;
            break;

            default:
                return QVariant();
        }
    }
...
};

When we compile and run the code, we won’t see any change. One might surmise that we need to modify the view (i.e. Subclassing QListView) and implementing a custom paint method. But wait, Qt has another approach: Delegate classes.

Rendering with a Delegate

The delegate can be used to implement a custom paint method for the items. To draw a custom item we need to implement a “paint” and a “sizeHint” method.

Delegate

Here a basic example to demonstrate the idea:

class ExampleDelegate :
    public QItemDelegate
{
    Q_OBJECT

    public:
    ExampleDelegate(QObject* parent = 0) :
        QItemDelegate(parent)
    {
    }

    // paint the pixmap and the two strings figure name and party name
    void paint (QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
    {
        if (index.isValid())
        {
            // get data from model
            QPixmap image = index.model()->data(index, Qt::DecorationRole).value<QPixmap>();
            QString figureName = index.model()->data(index, Qt::DisplayRole).toString();
            QString partyName  = index.model()->data(index, ListModel::DR_PARTYNAME).toString();

            // calculate positions for elements pixmap and strings
            QFont font = QApplication::font();
            QFontMetrics fm(font);
            // start at top left position
            QPoint imagePosition = option.rect.topLeft();
            QPoint firstNamePosition = imagePosition +     
				QPoint(15 /* an offset from left border*/, image.size().height()+fm.height());
            QPoint lastNamePosition  = firstNamePosition + QPoint(0, fm.height());

            // Draw image, first and last name
            painter->save();

            // draw image at position imagePosition
            painter->drawPixmap(imagePosition, image);

            // set font. Text black (normal) or red, if item selected
            painter->setFont(option.font);

            if (option.state & QStyle::State_Selected)
            {
                painter->setPen(QColor(Qt::red));
            }
            else
            {
                painter->setPen(QColor(Qt::black));
            }

            painter->drawText(firstNamePosition, figureName);
            painter->drawText(lastNamePosition,  partyName);
            painter->restore();
        }
    }

    // return the required space for the item (i.e. size for picture and two strings below)
    virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index ) const
    {
        Q_UNUSED(option);
        const QFont font = QApplication::font();
        const QFontMetrics fm(font);

        const QPixmap img = index.model()->data(index, Qt::DecorationRole).value<QPixmap>();
        return ( img.size()  // size of image
                 + QSize(0, 2*fm.height()));  // height of line 1 and 2
    }
};

To use the delegate we have to set it to the view. Our MainWindow class now looks like this:

MainWindow::MainWindow(QWidget* parent) :
    QMainWindow(parent)
{
    QListView* listView = new QListView(parent);
    setCentralWidget(listView);

    // Layout
    listView->setViewMode(QListView::IconMode); // display icons
    listView->setFlow (QListView::LeftToRight); //flow left to right
    listView->setWrapping(false); // no wrapping

    listView->setModel(new ListModel(parent)); // create and assign model
    ExampleDelegate* delegate = new ExampleDelegate(); // create the delegate
    listView->setItemDelegate(delegate); // assign the delegate
}

When we start it up the result look like this:

Picture2

Now we have the two lines below the pixmap.

Editing with a Delegate

A delegate can also be used to edit the content of an item. There are quite a few modifications, that need to be implemented.

First of all we have to extend the model to accept data and to write it to the item. This is done in the method “setData” (Here we just edit the figure name). We also need a method “flags”, which returns some attributes describing the abilities of the indexed item (here every item is editable).

ListModel2

class ListModel :
    public QAbstractItemModel
{
...
    virtual bool setData (const QModelIndex& index, const QVariant& value, int role = Qt::EditRole)
    {
        if (role == Qt::EditRole)
        {
            ListItem* item = mListItem.at(index.row());
            item->mFigureName = value.toString();
            return true;
        }
        return false;
    }

    virtual Qt::ItemFlags flags (const QModelIndex& index) const
    {
          Q_UNUSED(index);
          return Qt::ItemIsSelectable    |
                 Qt::ItemIsEditable      |
                 Qt::ItemIsEnabled;
    }
...
};

Now the modifications for the delegate:
We need a method for the creation of an editor when we double click an item. The method “createEditor” creates an editor widget (EditWidget) and sets the existing name of the figure.

Delegate2

We also need a method “updateEditorGeometry” which sets the editor widget to a position and size relative to the selected item (here the widget is placed over the upper half of the item).

Next we need “setModelData” to put back the changed data to the model. Somehow we have to inform the framework that we stopped editing and the data can be taken over. The signalization is done in the slot ”commitAndCloseEditor” (The editor emits a “editingFinished” signal, which is connected to this slot).

class ExampleDelegate :
    public QItemDelegate
{
...
    virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
    {
        Q_UNUSED(option);
        EditWidget* editor = new EditWidget(parent);
        const QString figureName = index.model()->data(index, Qt::DisplayRole).toString();
        editor->setText(figureName);
        Q_ASSERT(connect(editor, SIGNAL(editingFinished(EditWidget*)), SLOT(commitAndCloseEditor(EditWidget*))));
        return editor;
    }

    virtual void updateEditorGeometry (QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
    {
        Q_UNUSED(index);
        const QRect editorRect = option.rect;
        editor->setGeometry(editorRect.x(), editorRect.y(), editorRect.width(), editorRect.height()/2);
    }

    virtual void setModelData (QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
    {
        EditWidget* editWidget = qobject_cast<EditWidget*>(editor);
        const QString changedText = editWidget->getEditText();
        model->setData(index, changedText);
    }

    public slots:
    void commitAndCloseEditor(EditWidget* widget)
    {
        emit commitData(widget);
        emit closeEditor(widget);
    }
...
};

And here the implementation of the editor ():

// simple editor. No styling...
class EditWidget : 
	public QWidget
{
    Q_OBJECT
    public:
    EditWidget(QWidget* parent = 0) :
        QWidget(parent),
        edit(0)
    {
        QVBoxLayout* layout = new QVBoxLayout;
        setLayout(layout);
        edit = new QTextEdit(parent);
        layout->addWidget(edit);
        QPushButton* buttonFinish = new QPushButton("Finished", parent);
        layout->addWidget(buttonFinish);
        Q_ASSERT(connect(buttonFinish, SIGNAL(clicked()), SLOT(finished())));
    }

    public:
    void setText(const QString& text)
    {
        edit->setPlainText(text);
    }

    QString getEditText() const
    {
        return edit->toPlainText();
    }

    private:
    QTextEdit* edit;

    private slots:
    void finished()
    {
        emit editingFinished(this);
    }

    signals:
    void editingFinished(EditWidget* widget);
};

No modifications are necessary in the construction of the objects.

The result now looks like this when an item is double-clicked.

Picture3

The editor widget is created and drawn over the item.

Picture4

The changed data is taken over, when “finished” is clicked.

Summary

When you start using the interview framework there are a lot of pitfalls along the way. On one hand there is a great flexibility on how you represent your data in the view and how you manage them in your model. On the other hand there are quite a lot of nasty details you have to be aware of. They are very low level, meaning it is hard for new programmers to get their heads around.

Another issue with that concept is the responsibility of the delegate: Rendering, creating the editor, closing the editor, pushing back the edited data to the model is all crammed into one class, which is something you would not expect from a well-structured GUI framework.

This entry was posted in C++, UI and tagged . Bookmark the permalink.

3 Responses to Qt Interview Framework

    • Remy Mahler says:

      Thanks,
      I agree that the idea of separating the rendering functionality and the editing part of a delegate and putting them in separate classes could be an improvement.
      We would then see the delegate class as a kind of façade. The calls to the delegate would just be forwarded to the two specialized classes.
      On the other hand, if the implementation of the delegate class can be kept very simple (just a few lines of code), it would probably make no sense to add an additional indirection and to complicate the design unnecessarily.
      In my opinion, the decision on how to implement a delegate depends on the complexity, the focus and the architecture of the project.

Leave a Reply

Your email address will not be published. Required fields are marked *