QtQuick: Using Model/View for layout

July 4, 2012

When last we left our hero, we set up a simple (but not very scalable) Model/View using a C++ calculator engine and a QML UI. The “big” problem with it when you think of scalability is the fact that the UI is static. If the Engine wants to add buttons or change the function/role of the buttons — the UI has to be updated by a programmer. Instead, the UI should query query the Engine (or an abstraction of the Engine) to know the button layout.

In C++, we could do this by implementing a QAbstractTableModel and feed it to a pre-made (and extensible) widget like QTableView. In QML, the Model/View
options are less mature. For example, there is nothing in QML that is like a QTableView.[1] Therefore, we have to create our own.

Our strategy will be to create new classes ButtonLayoutModel (derived from QAbstractTableModel) and GridView (our custom QML Component). Our custom GridView will replace Qt’s Grid inside of Calculator.qml, and ButtonLayoutModel will be placed between Engine and the GridView. The messages passed will now be based on a “button ID” instead of the text that appears on the face of the button.

ButtonLayoutModel and Engine

The new class ButtonLayoutModel is pretty simple.

Excerpt of ButtonLayoutModel.hpp:

class ButtonLayoutModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    ButtonLayoutModel(QObject *parent = 0);
    ~ButtonLayoutModel();

public:
    /* Reimplemented virtual methods */
    virtual int columnCount(const QModelIndex& parent = QModelIndex()) const;
    virtual int rowCount(const QModelIndex& parent = QModelIndex()) const;
    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
    void setEngine(Engine *eng) {
        m_engine = eng;
    }

public slots:
    void buttonPressed(int id);

private:
    Engine *m_engine;

}; // class ButtonLayoutModel

Pretty simple… we implement only what we have to from QAbstractTableModel, an interface to set the underlying model, and a slot that makes things a little easier later.

Non-trivial excerpt of ButtonLayoutModel.cpp:

int ButtonLayoutModel::columnCount(const QModelIndex& /*parent*/) const
{
    return m_engine->columns();
}

int ButtonLayoutModel::rowCount(const QModelIndex& /*parent*/) const
{
    return m_engine->rows();
}

void ButtonLayoutModel::buttonPressed(int button_id)
{
    m_engine->event(button_id, Engine::E_BUTTON_CLICK);
}

QVariant ButtonLayoutModel::data(const QModelIndex& index, int role) const
{
    int button_id;

    button_id = m_engine->button_id(index.row(), index.column());
    switch (role) {
    case Qt::DisplayRole:
        return m_engine->button_text(button_id);
        break;
    case Qt::UserRole:
        return QVariant(button_id);
        break;
    }
    return QVariant();
}

The less obvious part here is how ButtonLayoutModel::data() works. We need to pass 2 different kinds of data from the Engine to the UI for layout: (a) the text to
display on the button (role == Qt::DisplayRole) and (b) the button’s ID (role == Qt::UserRole). While this doesn’t buy us much now… it could allow for the text on the button to be totally different than the buttons function (e.g. a non-ascii symbol, an icon, etc).

This requires some different support from Engine. Here are the changes:

diff -Nurp a/Engine.hpp b/Engine.hpp
--- a/Engine.hpp	2012-02-19 20:38:39.000000000 -0600
+++ b/Engine.hpp	2012-07-04 11:21:52.263084183 -0500
@@ -37,10 +37,28 @@ public:
         M_DIV,
     } mode_t;

+    typedef enum {
+        E_NONE = 0,
+        E_BUTTON_CLICK = 1,
+    } event_t;
+
     QString get_display();

+    int columns() {
+        return 4;
+    }
+    int rows() {
+        return 4;
+    }
+    /* zero-offset for row and col */
+    int button_id(int row, int col) {
+        return row*columns() + col;
+    }
+    QString button_text(int button_id);
+
 public slots:
     void keypress(QString val);
+    void event(int button_id, int event);

 signals:
     void content_changed(QString val);

diff -Nurp a/Engine.cpp b/Engine.cpp
--- a/Engine.cpp
+++ b/Engine.cpp
@@ -38,6 +38,42 @@ QString Engine::get_display()
     return m_display;
 }

+void Engine::event(int button_id, int ev)
+{
+    if (ev == E_BUTTON_CLICK) {
+        QString text = button_text(button_id);
+        keypress(text);
+    }
+    return;
+}
+
+QString Engine::button_text(int button_id)
+{
+    switch (button_id) {
+    case 0: return QString("7"); break;
+    case 1: return QString("8"); break;
+    case 2: return QString("9"); break;
+    case 3: return QString("+"); break;
+
+    case 4: return QString("4"); break;
+    case 5: return QString("5"); break;
+    case 6: return QString("6"); break;
+    case 7: return QString("-"); break;
+
+    case 8: return QString("1"); break;
+    case 9: return QString("2"); break;
+    case 10: return QString("3"); break;
+    case 11: return QString("*"); break;
+
+    case 12: return QString("C"); break;
+    case 13: return QString("0"); break;
+    case 14: return QString("="); break;
+    case 15: return QString("/"); break;
+    }
+    return QString();
+}
+
+
 void Engine::keypress(QString val)
 {
     qDebug() << "Got keypress " << val;

Nothing special here. Just added the start of an event-based system.

GridView

For this next part, let’s clarify three concepts for this specific part of the project:

  • Model – The abstract interface to the core application logic (e.g. the data of a spreadsheet).
  • View – A container that provides a graphical representation of the contents of the Model (e.g. the rows and columns that you see in a spreadsheet).
  • Delegate – A graphical widget that is used to display and interact with an specific item in the Model (e.g. the widget used for each cell in the spreadsheet).

The View must be given two things: a Model and a Delegate. For every item in the Model, it will create a new instance of the Delegate for interacting with the model. In our case, the Model is the ButtonLayoutModel presented above, and the Delegate is the Button QML element.

And since our UI is in QML, our View must be a QML Element.

Here’s the meat of GridView.hpp:

class GridView : public QDeclarativeItem
{
    Q_OBJECT
    Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
    Q_PROPERTY(QDeclarativeComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
    Q_PROPERTY(float margin READ margin WRITE setMargin);

public:
    GridView(QDeclarativeItem* parent = 0);
    ~GridView();

    QVariant model() const;
    void setModel(const QVariant& mod);

    QDeclarativeComponent* delegate() const;
    void setDelegate(QDeclarativeComponent* del);

    float margin() const;
    void setMargin(float m);

signals:
    void modelChanged();
    void delegateChanged();
    void cellWidthChanged();
    void cellHeightChanged();

private:
    QAbstractItemModel *m_model;
    QDeclarativeComponent *m_delegate;
    float m_margin;

protected slots:
    /* Slots to be handled QAbstractItemModel */
    void columnsInserted(const QModelIndex& parent, int start, int end);
    void columnsMoved(const QModelIndex& sourceParent, int sourceStart, int sourceEnd,
                      const QModelIndex& destinationParent, int destinationColumn);
    void columnsRemoved(const QModelIndex& parent, int start, int end);
    void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight);
    void headerDataChanged(Qt::Orientation orientation, int first, int last);
    void layoutChanged();
    void modelReset();
    void rowsInserted(const QModelIndex& parent, int start, int end);
    void rowsMoved(const QModelIndex& sourceParent, int sourceStart, int sourceEnd,
                   const QModelIndex& destinationParent, int destinationRow);
    void rowsRemoved(const QModelIndex& parent, int start, int end);

protected:
    virtual void geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry);

private:
    void update_layout();

}; // class GridView

Most of this is scaffolding and functions that were required to be re-implemented. The most important part is the properties added at the top: model, delegate, and margin. These translate exactly to QML properties and will be passed to us at run-time from the QML script.

One of the tricky parts is getting/setting the model:

QVariant GridView::model() const
{
    return *reinterpret_cast<QVariant*>(m_model);
}

Note: I think using reinterpret_cast<> is a mistake and that it should be some manner of qvariant_cast<> like in setModel(). (I don’t recall, it’s been a few months since I wrote this part!)

#define M_CONNECT(sender, sig) connect((sender), SIGNAL(sig), this, SLOT(sig))
#define M_DISCONNECT(sender, sig) connect((sender), SIGNAL(sig), this, SLOT(sig))

void GridView::setModel(const QVariant& model)
{
    QObject *obj;
    QAbstractItemModel *mm;
    std::cout << __func__ << std::endl;

    obj = qvariant_cast<QObject*>(model);
    mm = qobject_cast<QAbstractItemModel*>(obj);

    if (!mm) {
        std::cerr << "Can not set model... is not a QAbstractItemModel" << std::endl;
        return;
    }

    if (m_model) {

        M_DISCONNECT(m_model,
                     columnsInserted(const QModelIndex&, int, int));
        M_DISCONNECT(m_model,
                     columnsMoved(const QModelIndex&, int, int, const QModelIndex&, int));
        M_DISCONNECT(m_model,
                     columnsRemoved(const QModelIndex&, int, int));
        M_DISCONNECT(m_model,
                     dataChanged(const QModelIndex&, const QModelIndex&));
        M_DISCONNECT(m_model,
                     headerDataChanged(Qt::Orientation, int, int));
        M_DISCONNECT(m_model,
                     layoutChanged());
        M_DISCONNECT(m_model,
                     modelReset());
        M_DISCONNECT(m_model,
                     rowsInserted(const QModelIndex&, int, int));
        M_DISCONNECT(m_model,
                     rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int));
        M_DISCONNECT(m_model,
                     rowsRemoved(const QModelIndex&, int, int));
    }

    m_model = mm;

    M_CONNECT(m_model,
              columnsInserted(const QModelIndex&, int, int));
    M_CONNECT(m_model,
              columnsMoved(const QModelIndex&, int, int, const QModelIndex&, int));
    M_CONNECT(m_model,
              columnsRemoved(const QModelIndex&, int, int));
    M_CONNECT(m_model,
              dataChanged(const QModelIndex&, const QModelIndex&));
    M_CONNECT(m_model,
              headerDataChanged(Qt::Orientation, int, int));
    M_CONNECT(m_model,
              layoutChanged());
    M_CONNECT(m_model,
              modelReset());
    M_CONNECT(m_model,
              rowsInserted(const QModelIndex&, int, int));
    M_CONNECT(m_model,
              rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int));
    M_CONNECT(m_model,
              rowsRemoved(const QModelIndex&, int, int));

    update_layout();
    emit modelChanged();
}

I.e. the tricky part is the casting, and then we connect all the signals and slots to our own model. Notice, too, that there is nothing here that is specific to our ButtonLayoutModel.

void GridView::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry)
{
    qDebug() << oldGeometry << " ==> " << newGeometry;
    setImplicitWidth(newGeometry.width());
    setImplicitHeight(newGeometry.height());
    update_layout();
}

void GridView::update_layout()
{
    QObjectList kids = children();
    int rows, cols, r, c;
    float x, y, margin;
    float cell_height, cell_width;
    QObject *obj;
    QModelIndex ix;

    if (!m_model || !m_delegate)
        return;

    /* clear out existing children */
    foreach(obj, kids) {
        obj->setParent(0);
        delete obj;
    }

    rows = m_model->rowCount();
    cols = m_model->columnCount();
    cell_height = implicitHeight() / rows;
    cell_width = implicitWidth() / cols;
    margin = GridView::margin();

    y = margin/2;
    for (r = 0 ; r < rows ; ++r) {
        x = margin/2;
        for (c = 0 ; c < cols ; ++c) {
            obj = m_delegate->create();
            obj->setParent(this);
            obj->setProperty("parent", QVariant::fromValue<QDeclarativeItem*>(this));
            obj->setProperty("x", x);
            obj->setProperty("y", y);
            obj->setProperty("width", cell_width - margin);
            obj->setProperty("height", cell_height - margin);
            ix = m_model->index(r, c);
            obj->setProperty("text", m_model->data(ix, Qt::DisplayRole));
            obj->setProperty("button_id", m_model->data(ix, Qt::UserRole));
            connect(obj, SIGNAL(buttonPressed(int)),
                    m_model, SLOT(buttonPressed(int)));
            x += cell_width;
        }
        y += cell_height;
    }

}

This update_layout() function deletes all the delegates and then allocates new ones every time it is called. Obviously this is not ideal, but it was simple to implement. However, after creating each delegate:

  • It sets its location and size (layout) via properties
  • It sets some properties that are specific to our application… button_id and text. Note that these are very specific to our Button.qml implementation.
  • It connects a signal to the m_model slot. This, too, is very specific to our Button and ButtonLayoutModel implementations.

Most of the functions not shown simply call update_layout() — not ideal, but it works for now. 🙂

Changes to Button

Button almost works as-is. The only change is to add the ‘button_id’ property and change the signal to send an integer instead of text.

diff -Nurp a/Button.qml b/Button.qml
--- a/Button.qml
+++ b/Button.qml
@@ -10,6 +10,7 @@ import QtQuick 1.0
 Item {
     /* This should be set by the parent element */
     property string text: "X"
+    property int button_id: 0

     /* These should EXIST in the parent element, or be set
      * BY the parent element.  They are referred to by the
@@ -26,9 +27,9 @@ Item {
     property int radius: (height < width) ? height/8 : width/8;

     /* This signal will fire when we get clicked, and contain the
-     * text of the button.
+     * id
      */
-    signal postValue(string val);
+    signal buttonPressed(int id);

     /* Main geometry of button */
     Rectangle {
@@ -57,7 +58,7 @@ Item {
 	id: iMouseArea;
 	anchors.fill: parent;
 	onClicked: {
-	    postValue(text);
+	    buttonPressed(button_id);
 	}
     }
 }

Changes to Calculator

In Calculator we do the following:

  • Declare our custom components as ‘Local’
  • Remove all our hard-coded button stuff
  • Replace Grid with Local.GridView
  • Set up a delegate (Button) for the GridView
diff -Nurp a/Calculator.qml b/Calculator.qml
--- a/Calculator.qml
+++ b/Calculator.qml
@@ -2,9 +2,11 @@
  */

 import QtQuick 1.0
+import "." as Local
+import Foo 1.0 as Local

 Rectangle {
     id: iRoot;
@@ -54,43 +56,29 @@ Rectangle {
 	}
     }

-    Grid {
-	columns: 4;
-	property int button_width: (width - 2 * anchors.margins)/columns;
-	property int button_height: button_width;
-	property color button_color_top: "#8888FF";
-	property color button_color_bot: "blue";
+    Component {
+	id: iButtonDelegate;
+
+	Button {
+	    
+	}
+    }
+
+    Local.GridView {
+	id: button_grid;
+	property color button_color_top: "white";
+	property color button_color_bot: "gray";
 	property color text_color: "black";
-	spacing: parent.anchors.margins;
+
 	anchors.top: iDisplay.bottom;
 	anchors.left: iRoot.left;
 	anchors.right: iRoot.right;
 	anchors.bottom: iRoot.bottom;
 	anchors.margins: parent.anchors.margins;

-	/* Row 0 */
-	Button { id: i7; text: "7"; }
-	Button { id: i8; text: "8"; }
-	Button { id: i9; text: "9"; }
-	Button { id: iPlus; text: "+"; }
-
-	/* Row 1 */
-	Button { id: i4; text: "4"; }
-	Button { id: i5; text: "5"; }
-	Button { id: i6; text: "6"; }
-	Button { id: iMinus; text: "-"; }
-
-	/* Row 2 */
-	Button { id: i1; text: "1"; }
-	Button { id: i2; text: "2"; }
-	Button { id: i3; text: "3"; }
-	Button { id: iMultiply; text: "*"; }
-
-	/* Row 3 */
-	Button { id: iC; text: "C"; }
-	Button { id: i0; text: "0"; }
-	Button { id: iEquals; text: "="; }
-	Button { id: iDivide; text: "/"; }
+	model: button_model;
+	delegate: iButtonDelegate;
+	margin: 7;

     }

@@ -151,25 +139,4 @@ Rectangle {
             break;
 	}
     }
-
-    /* Component.onCompleted() is more or less a constructor */
-    Component.onCompleted: {
-	/* N.B. Using 'iRoot.' here is redundant. */
-	iC.postValue.connect(iRoot.data);
-	iPlus.postValue.connect(iRoot.data);
-	iMinus.postValue.connect(iRoot.data);
-	iMultiply.postValue.connect(iRoot.data);
-	iDivide.postValue.connect(iRoot.data);
-	iEquals.postValue.connect(iRoot.data);
-	i0.postValue.connect(iRoot.data);
-	i1.postValue.connect(iRoot.data);
-	i2.postValue.connect(iRoot.data);
-	i3.postValue.connect(iRoot.data);
-	i4.postValue.connect(iRoot.data);
-	i5.postValue.connect(iRoot.data);
-	i6.postValue.connect(iRoot.data);
-	i7.postValue.connect(iRoot.data);
-	i8.postValue.connect(iRoot.data);
-	i9.postValue.connect(iRoot.data);
-    }
 }

Putting it all together

In order to use our ‘Local’ QML Component library, we must declare it early in our main() function. We also need to plug our model into the Calculator QML.

diff -Nurp a/main.cpp b/main.cpp
--- a/main.cpp	2012-02-19 20:38:39.000000000 -0600
+++ b/main.cpp	2012-07-04 09:36:43.527800857 -0500
@@ -19,9 +19,12 @@

 #include <QtGui/QApplication>
 #include <QtDeclarative/QDeclarativeView>
+#include <QtDeclarative/QDeclarativeContext>

 #include "Delegate.hpp"
 #include "Engine.hpp"
+#include "ButtonLayoutModel.hpp"
+#include "GridView.hpp"

 int main(int argc, char* argv[])
 {
@@ -29,8 +32,17 @@ int main(int argc, char* argv[])
     QDeclarativeView view;
     Engine engine;
     Delegate del;
+    ButtonLayoutModel blm;
+    QDeclarativeContext *ctxt;

+    qmlRegisterType<GridView>("Foo", 1, 0, "GridView");
+
+    blm.setEngine(&engine);
+
+    ctxt = view.rootContext();
+    ctxt->setContextProperty("button_model", &blm);
     view.setSource(QUrl::fromLocalFile("Calculator.qml"));
+
     del.set_view(&view);
     del.set_engine(&engine);
     del.init();

And once again we have our underwhelming calculator working. 🙂

Critical Thinking

Again, what are the shortcomings of what we’ve done?

  • GridView still needs special knowledge of both the Model and the View in order for this to work. On the one hand it would be best if GridView were a perfectly pure container (like QTableView) — on the other hand a semi-specialized container can also have its benefits.
  • The GridView is automatically connecting signals and slots. This is typically bad form and totally circumvents the Delegate class that was originally set up for this kind of thing. It might have been better for the delegate to connect its
    signals to the parent (Calculator, channeling all the events through that interface.
  • The way that data is handled between the Button delegate and the Model seems a little clunky. It might have been better to pass the Button special access to the Model to figure out what data it needs. But I’m not sure how to do
    this since Button is a QML object. And again, this is simply an attempt to make GridView fully generic.
  • But when you get right down to it… I’m still not happy with the fact that I had to write my own GridView.

While some of the interactions in this example are a tad hack-ish, we’ve accomplished the goal of having the core logic in the Engine and having the UI adapt as the Engine changes.

Resources

All of the sources can be found at http://gabe.is-a-geek.org/blog_content/2012/07/04-qml-cpp-link/ The tarball calculator.tar.bz2 has everything… but you’ll also find individual files.


The code in this article is a mixture of Public Domain code and
GPL code. Please see the files in the Resources section for
specifics.

[1] – There is a Grid Element, but it expects
the underlying model to be 1D, and it lays them out in a 2D array. We
need the underlying model to be 2D. In general, QML “Models” are all
1D. There might be some things that could be used in Qt Desktop
Components or even the MeeGo Components… but the documentation on
those is sparse and you would have to compile them yourself.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: