QtQuick: QML-to-C++ Link

January 9, 2012

Last time we set up a QML scaffolding for a Calculator UI. The top-level UI set up the following signals and slots to communicate with the Calculator engine:

  • data(string val) – (UI to Engine) communicates which button is pushed
  • contentChanged(string val) – (Engine to UI) indicates that the display text has changed

So, our application is going to be architectured like this:

  • View – The QML UI
  • Model – The calculator Engine
  • main() – This will glue everything together

The Engine Class

For the purpose of communicating with the QML UI… the main content of class Engine is:

class Engine : public QObject
{
    Q_OBJECT

    /* ... */

public slots:
    void keypress(QString val);

signals:
    void content_changed(QString val);

    /* ... */
};

Everything else is just implementation of the logic. When the user presses a button, it is communicated to the engine via the keypress(QString) slot. Whenever the engine decides that the UI’s display should be changed, it communicates it with the content_changed(QString) signal.

See the link at the end for the full implementation, but the calculator is implemented as a state machine. The current operation is the current state (or “mode”). It changes behavior based on the current mode and whether or not the mode has recently changed. However, since the focus is C++/QML interaction… we won’t go into the Engine details.

main(): Making Connections

To glue together the Engine and the UI, we need to set things up inside the main() function:

/*
 * main.cpp (calculator)
 *
 * This code was written by Gabriel M. Beddingfield <gabrbedd@gmail.com>
 * in 2011-2012.  It is placed in the public domain.
 */

#include <QtGui/QApplication>
#include <QtGui/QGraphicsObject>
#include <QtDeclarative/QDeclarativeView>

#include "Engine.hpp"

int main(int argc, char* argv[])
{
    QApplication qapp(argc, argv);
    QDeclarativeView view;
    Engine engine;

    view.setSource(QUrl::fromLocalFile("Calculator.qml"));

    /* This sets up the connections between the view and engine */
    {
        QObject *v = view.rootObject();
        QObject *e = &engine;

        QObject::connect(v, SIGNAL(data(QString)),
                         e, SLOT(keypress(QString)));
        QObject::connect(e, SIGNAL(content_changed(QString)),
                         v, SLOT(contentChanged(QString)));
        QObject::connect(v, SIGNAL(quit()),
                         &view, SLOT(close()));
    }

    view.show();

    return qapp.exec();
}

This simply sets up an Engine, sets up the QML View (QDeclarativeView), and then sets up the signals and slots.

There’s two Gotcha!‘s in the code:

  1. Connections to the QML UI are not made with the QDeclarativeView, but with QDeclarativeView::rootObject() (which has type QGraphicsView).
  2. Because of the QGraphicsView object, you need to include the <QGraphicsView> header,[1] or else your compiler will give you strange errors about casting.
  3. The close() slot, however, is in the QDeclarativeView. In this case we connect our QML UI to its QDeclarativeView container.

QML Changes

On the QML side of things, there’s a few changes necessary in order to get the ball rolling.

  • Remove scaffolding: The stuff that we marked as “THIS IS TEMPORARY” last time needs to be removed since we have a bona fide Engine, now.
  • Button changes: Our implementation last time didn’t really have enough buttons to even be a simple “adding only” calculator. We’ve added more buttons and rearranged them.
  • Keyboard accelerators: With the loss of things like QAction, what do we do for touch-typing-junkies like me?

The first two (scaffolding and buttons) is pretty easy:

--- 00.Calculator.qml
+++ 01.Calculator.qml
@@ -10,25 +10,18 @@
 Rectangle {
     id: iRoot;
     color: "gray";
-    width: 200; height: 310;
+    width: 200; height: 252;
     anchors.margins: 5;
     property string text: "0";

     signal data(string val);
     onData: {
 	console.log("iRoot: " + val)
-	/* XXX THIS IS TEMPORARY */
-	contentChanged(val);
     }

     signal contentChanged(string val);
     onContentChanged: {
-	/* XXX THIS IS TEMPORARY */
-	if (text == "0") {
-	    text = val;
-	} else {
-	    text += val;
-	}
+	text = val;
     }

     signal clear;
@@ -59,8 +52,8 @@
     }

     Grid {
-	columns: 3;
-	property int button_width: (width - 2 * anchors.margins)/3;
+	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";
@@ -76,29 +69,37 @@
 	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: iPlus; text: "+"; }
+	Button { id: iEquals; text: "="; }
+	Button { id: iDivide; text: "/"; }

     }

     /* Component.onCompleted() is more or less a constructor */
     Component.onCompleted: {
 	/* N.B. Using 'iRoot.' here is redundant. */
-	iC.postValue.connect(iRoot.clear);
+	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);

We simply deleted some stuff, added the extra buttons, and plugged up the internal signal/slot connections. Actually, these changes alone will give you a fine, functioning calculator.

But support for keystroke entry is important to people like me. That’s one reason why I really miss QAction. However, it’s not terribly difficult to add in support for keystrokes in our calculator, using the Keys element:

--- 01.Calculator.qml
+++ Calculator.qml
@@ -13,6 +13,7 @@
     width: 200; height: 252;
     anchors.margins: 5;
     property string text: "0";
+    focus: true; /* for keyboard input */

     signal data(string val);
     onData: {
@@ -29,6 +30,8 @@
 	text = "0";
     }

+    signal quit;
+
     Rectangle {
 	id: iDisplay;
 	color: "white";
@@ -91,6 +94,64 @@

     }

+    Keys.onPressed: {
+	switch (event.key) {
+	case Qt.Key_0:
+            iRoot.data("0");
+            break;
+	case Qt.Key_1:
+            iRoot.data("1");
+            break;
+	case Qt.Key_2:
+            iRoot.data("2");
+            break;
+	case Qt.Key_3:
+            iRoot.data("3");
+            break;
+	case Qt.Key_4:
+            iRoot.data("4");
+            break;
+	case Qt.Key_5:
+            iRoot.data("5");
+            break;
+	case Qt.Key_6:
+            iRoot.data("6");
+            break;
+	case Qt.Key_7:
+            iRoot.data("7");
+            break;
+	case Qt.Key_8:
+            iRoot.data("8");
+            break;
+	case Qt.Key_9:
+            iRoot.data("9");
+            break;
+	case Qt.Key_C:
+            iRoot.data("C");
+            break;
+	case Qt.Key_Plus:
+            iRoot.data("+");
+            break;
+	case Qt.Key_Minus:
+            iRoot.data("-");
+            break;
+	case Qt.Key_Asterisk:
+            iRoot.data("*");
+            break;
+	case Qt.Key_Slash:
+            iRoot.data("/");
+            break;
+	case Qt.Key_Equal:
+	case Qt.Key_Enter:
+	case Qt.Key_Return:
+            iRoot.data("=");
+            break;
+	case Qt.Key_Escape:
+            iRoot.quit();
+            break;
+	}
+    }
+
     /* Component.onCompleted() is more or less a constructor */
     Component.onCompleted: {
 	/* N.B. Using 'iRoot.' here is redundant. */

We simply set up a look-up table, so that when a key is pressed it does the same thing as if you had pushed a button. We’ve also mapped the Escape key to a quit signal (which we connect in main()).

Critical Thinking

But if we sit down and think about this Calculator implementation… what are its shortcomings? (And remember, we’re focusing on code relationships… not how sexy it looks.)

  • The UI and Engine communicate through an ASCII protocol. We know that this doesn’t scale well, nor does it lend itself to flexibility or internationalization. Nor does it handle the addition or removal of buttons (e.g. for different calculator modes).
  • The Engine always communicates its entire state to the UI. This state is passed via signal/slot parameters. As the state gets larger, this will bog things down. (I.e. this doesn’t scale well, either.)
  • The connection between the UI and Engine are managed by main(), where we manually connected all the signals and slots. This is a bit error-prone and is not dynamic.

Can you think of any other shortcomings in this architecture? We’ll talk about them in following blog posts.

Resources

calculator.tar.bz2 — All the code (tarball).

calculator.pro — The QMake project file

Calculator.qml — The QML UI

Button.qml — The Button Component

Engine.hpp — The Engine declaration

Engine.cpp — The Engine implementation

main.cpp — The main module

[1] You might notice that my headers are <QtGui/QGraphicsObject>, including the module. I’ve found that this makes things a little easier when you’re not using QMake. For example, CMake 2.8 doesn’t currently support configuration for the QDeclarative module. Adding the module to the header makes that a little easier.