QtQuick: Prototyping a Calculator

December 27, 2011

This is not going to be a tutorial on QML. There are plenty of those. (See [2])

However, this will discuss some of the problems I encountered while implementing a calculator application. The purpose of this application is /not/ to make a calculator. The purpose is to explore C++/QtQuick interactions as they would happen in a large project.

So, the goal is to implement this ugly little calculator:

We’ll implement as much of it as we can in QML, but we won’t actually make it functional. (E.g. button presses with do “something”… but not necessarily the “right” thing.) So, here’s a quick scaffolding:[1]

/*                                      -*- mode:javascript -*-
 * Calculator.00.qml - a scaffolding for a calculator UI.
 *
 * This code was written by Gabriel M. Beddingfield <gabrbedd@gmail.com>
 * in 2011.  It is placed in the public domain.
 */

import QtQuick 1.0

Rectangle {
    id: iRoot;
    color: "gray";
    width: 200; height: 310;
    anchors.margins: 5;
    property string text: "0";

    Rectangle {
	id: iDisplay;
	color: "white";
	anchors.top: parent.top;
	anchors.left: parent.left;
	anchors.right: parent.right;
	anchors.margins: parent.anchors.margins;
	height: 42;
	radius: height/10;

	Text {
	    id: iDisplayText;
	    text: iRoot.text;
	    color: "black";
	    font.pixelSize: 24;
	    anchors.verticalCenter: parent.verticalCenter;
	    anchors.right: parent.right;
	    anchors.margins: 10;

	}
    }

    Grid {
	columns: 3;
	property int bw: (width - 2 * anchors.margins)/3;
	property int bh: bw;
	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 */
	Rectangle { id: i7; width: parent.bw; height: parent.bh; }
	Rectangle { id: i8; width: parent.bw; height: parent.bh; }
	Rectangle { id: i9; width: parent.bw; height: parent.bh; }

	/* Row 1 */
	Rectangle { id: i4; width: parent.bw; height: parent.bh; }
	Rectangle { id: i5; width: parent.bw; height: parent.bh; }
	Rectangle { id: i6; width: parent.bw; height: parent.bh; }

	/* Row 2 */
	Rectangle { id: i1; width: parent.bw; height: parent.bh; }
	Rectangle { id: i2; width: parent.bw; height: parent.bh; }
	Rectangle { id: i3; width: parent.bw; height: parent.bh; }

	/* Row 3 */
	Rectangle { id: iC; width: parent.bw; height: parent.bh; }
	Rectangle { id: i0; width: parent.bw; height: parent.bh; }
	Rectangle { id: iPlus; width: parent.bw; height: parent.bh; }
    }
}

This is something that anyone should be able to do after going through one of the QtQuick tutorials.[2] So, if this part makes your head spin, then you should go visit a more basic tutorial first.

You may notice that I’m using Hungarian notation for all of the Item id’s. (E.g. “id: iRoot;”). The reason is that this id name can be used anywhere, any time to refer to this object. Even in another QML file. I find this really confusing, and the “i” helps clear things up pretty quick.[3]

Abstracting the Buttons

Every button on the calculator has several things in common:

  • They should all have an identical style.
  • They all represent one thing (like the number ‘5’)
  • When the user presses them, we want an event to notify us. Otherwise, we don’t really care how the button works.
  • We know that we need at least 12 of them.

If you’ve done Qt programming, you might think, “Easy… just use a QPushButton.” However, there is not such animal in QML. So right off the bat, we’re needing to make a custom widget. At first this may seem like a pain, but the truth is that in C++ we would also need a custom widget. We just wouldn’t write it until later because it’s extra work.

The design is pretty simple. We want the button to configure itself and send us a signal when it gets clicked. In other words here’s all we really care about:

Item {
    property string text: "X"

    signal postValue(string val);

    /* ...*/
}

Beyond that, we don’t really care what it does or how it works, right? The rest of it is really internal. So a no-frills button would look like this. Note: it’s very important that the file be named “Button.qml”. It’s not just the file-name, that’s how you name your custom element. Here’s the implementation:

/*                                      -*- mode:javascript -*-
 * Button.qml
 *
 * This code was written by Gabriel M. Beddingfield <gabrbedd@gmail.com>
 * in 2011.  It is placed in the public domain.
 */

import QtQuick 1.0

Item {
    /* This should be set by the parent element */
    property string text: "X"

    /* These should EXIST in the parent element, or be set
     * BY the parent element.  They are referred to by the
     * child elements.
     */
    height: parent ? parent.button_height : 42;
    width: parent ? parent.button_width : 42;
    property color button_color_top: parent ? parent.button_color_top : "white";
    property color button_color_bot: parent ? parent.button_color_bot : "gray";
    property color text_color: parent ? parent.text_color : "black";

    /* These properties may be set by the parent element */
    property int font_pixel_size: 24;
    property int radius: (height < width) ? height/8 : width/8;

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

    /* Main geometry of button */
    Rectangle {
	id: iRectangle;
	anchors.fill: parent; /* parent is iRoot */
	radius: parent.radius;

	gradient: Gradient {
	    /* parent.button_color_* is implied */
	    GradientStop { position: 0.0; color: button_color_top; }
	    GradientStop { position: 1.0; color: button_color_bot; }
	}
    }

    /* Text display of button */
    Text {
	id: iText;
	text: parent.text; /* without 'parent.' this would be ambiguous */
	color: text_color; /* parent.text_color implied */
	font.pixelSize: font_pixel_size;
	anchors.centerIn: parent;
    }

    /* This enables mouse interaction with button */
    MouseArea {
	id: iMouseArea;
	anchors.fill: parent;
	onClicked: {
	    postValue(text);
	}
    }
}

This implementation shouldn’t be too much of a surprise. Here’s a few things to note:

  • The base thing is Item. The Rectangle is actually an implementation detail. By making the base an Item, we’re free to (for example) rotate the Rectangle 90° in order to get a left-to-right gradient. Now the Item is a meta-object the contains mostly meta-data (including the signal).
  • Every button needs an identical height, width, color, etc. Instead of copying the code over and over in Calculator.qml, I have the Button query the parent object for what these properties should be.
  • QML’s scoping rules allow us to access the parent’s properties and variables without qualifying them with “parent.”. However, in some cases this is ambiguous and we have to explicitly reference ‘parent.’ Whether you always use “parent” or not is a matter of style.
  • When the MouseArea “clicked” signal fires, we override its “onClicked” method to fire our own custom signal. We need to do this so that the button’s value can be sent in the signal.

Try out Button.qml using the qmlviewer test app:

    $ qmlviewer Button.qml

Putting the Buttons in the Calculator

Now we plug it in to our calculator. We also add a ‘color’ property to the grid to make it easier for the buttons to auto-configure themselves.[4] Here’s the changes we make:

--- Calculator.00.qml
+++ Calculator.01.qml
@@ -1,5 +1,5 @@
 /*                                      -*- mode:javascript -*-
- * Calculator.00.qml - a scaffolding for a calculator UI.
+ * Calculator.01.qml - Uses the Button element.
  *
  * This code was written by Gabriel M. Beddingfield <gabrbedd@gmail.com>
  * in 2011.  It is placed in the public domain.
@@ -38,8 +38,11 @@

     Grid {
 	columns: 3;
-	property int bw: (width - 2 * anchors.margins)/3;
-	property int bh: bw;
+	property int button_width: (width - 2 * anchors.margins)/3;
+	property int button_height: button_width;
+	property color button_color_top: "#8888FF";
+	property color button_color_bot: "blue";
+	property color text_color: "black";
 	spacing: parent.anchors.margins;
 	anchors.top: iDisplay.bottom;
 	anchors.left: iRoot.left;
@@ -48,23 +51,23 @@
 	anchors.margins: parent.anchors.margins;

 	/* Row 0 */
-	Rectangle { id: i7; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i8; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i9; width: parent.bw; height: parent.bh; }
+	Button { id: i7; text: "7"; }
+	Button { id: i8; text: "8"; }
+	Button { id: i9; text: "9"; }

 	/* Row 1 */
-	Rectangle { id: i4; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i5; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i6; width: parent.bw; height: parent.bh; }
+	Button { id: i4; text: "4"; }
+	Button { id: i5; text: "5"; }
+	Button { id: i6; text: "6"; }

 	/* Row 2 */
-	Rectangle { id: i1; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i2; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i3; width: parent.bw; height: parent.bh; }
+	Button { id: i1; text: "1"; }
+	Button { id: i2; text: "2"; }
+	Button { id: i3; text: "3"; }

 	/* Row 3 */
-	Rectangle { id: iC; width: parent.bw; height: parent.bh; }
-	Rectangle { id: i0; width: parent.bw; height: parent.bh; }
-	Rectangle { id: iPlus; width: parent.bw; height: parent.bh; }
+	Button { id: iC; text: "C"; }
+	Button { id: i0; text: "0"; }
+	Button { id: iPlus; text: "+"; }
     }
 }

The calculator doesn’t do anything yet, but it now looks the way that we want it. We simply replaced the Rectangle items with our custom Button elements.

I’ve not yet decided if using parent.button_height and friends is good design or not, but it at least saves a bunch of copy-and-paste.

Try out Calculator.01.qml using the qmlviewer test app:

      $ qmlviewer Calculator.01.qml

Note that you must also have Button.qml in the same folder.

Prototyping the interface

To get this data to our C++ app, we’ll need to set up the signals and slots that we’ll use to communicate with the core model of the application. What we need is:

  • A signal from the UI to the engine when a button is pressed, and which will carry the data that the button represents. We’ll call the signal ‘data
  • A signal from the engine to the UI to that contains the display text. We’ll call the signal ‘contentChanged
  • A signal from the UI to the engine when the “clear” button has been pressed. We’ll call the signal ‘clear
  • Something that sets up a connection from each button to the main ‘data‘ signal.

Let’s start with just the data signal. When you declare a signal (named ‘foo‘) then there is automatically a slot created called ‘onFoo‘).[5] Not only that, but if the signal was declared with an argument, the argument (and its name) is assumed when you implement the slot. So, for data:

    signal data(string val);
    onData: {
	console.log("iRoot: " + val)
    }

    /* ... */

    /* Component.onCompleted() is more or less a constructor */
    Component.onCompleted: {
	iPlus.postValue.connect(data);
	i0.postValue.connect(data);
	i1.postValue.connect(data);
	i2.postValue.connect(data);
	i3.postValue.connect(data);
	i4.postValue.connect(data);
	i5.postValue.connect(data);
	i6.postValue.connect(data);
	i7.postValue.connect(data);
	i8.postValue.connect(data);
	i9.postValue.connect(data);
    }

This is pretty simple… create the signal ‘data‘, on the slot log which button was pushed, and connect the signal/slot in the Component.onCompleted constructor-like-slot.

So, we finish out the design by implementing them all. All the slots will do something, just so that we get an indication that they are firing correctly. Building on top of Calculator.01.qml:

--- Calculator.01.qml
+++ Calculator.qml
@@ -1,5 +1,5 @@
 /*                                      -*- mode:javascript -*-
- * Calculator.01.qml - Uses the Button element.
+ * calculator.qml
  *
  * This code was written by Gabriel M. Beddingfield <gabrbedd@gmail.com>
  * in 2011.  It is placed in the public domain.
@@ -14,6 +14,28 @@
     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;
+	}
+    }
+
+    signal clear;
+    onClear: {
+	text = "0";
+    }
+
     Rectangle {
 	id: iDisplay;
 	color: "white";
@@ -69,5 +91,23 @@
 	Button { id: iC; text: "C"; }
 	Button { id: i0; text: "0"; }
 	Button { id: iPlus; text: "+"; }
+
+    }
+
+    /* Component.onCompleted() is more or less a constructor */
+    Component.onCompleted: {
+	/* N.B. Using 'iRoot.' here is redundant. */
+	iC.postValue.connect(iRoot.clear);
+	iPlus.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);
     }
 }

The temporary code is just to make the Calculator semi-functional, until we put the real brains into it. Try it out like this:

      $ qmlviewer Calculator.qml

Note that we could extend the prototype by adding JavaScript application logic (like several of the Qt Quick examples). However, since the goal is to connect a C++ engine to a QML UI, this would be a waste of time.

Summary

You might be asking, “where’s the C++ code?” We’ll do that next time. Hopefully, you can already see that we’ve set up a well-defined interface that we can use to connect our calculator brains to.

The biggest things that I learned in this part of the exercise are:

  • When making a custom component, it is generally better to use ‘Item’ as the base element of your component.
  • Use the Component.onCompleted method do internal signal/slot connections and any other init code.
  • Signals in QML will also implicitly create a slot. The slot function has some special “magic” in the way it is declared and over-ridden.
  • The scope of variables, properties, and id’s is kind of sloppy. Judicious use of Hungarian notation and object dereferences can make the code more readable.

Source Files

Button.qml — The Button abstraction.

Calculator.00.qml — The first Calculator iteration (layout)

Calculator.01.qml — The second Calculator iteration (using Button)

Calculator.qml — The final Calculator

[1] See the file 00_scaffolding.qml

[2] See Getting Started Programming with QML and Qt Quick …which also contains links to examples.

[3] “thingamajig.trigger()” — what the heck is “thingamajig???”

[4] I could have done it with width/height, too.

[5] If you, like me, don’t like camelCasing, getUsedToIt.

Advertisements

One Response to “QtQuick: Prototyping a Calculator”

  1. Thanks! This is very useful

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: