Interactive UI with Multiple Top-Level Windows

Now that our application is becoming more complete, we need to add some functionality to make it usable on a daily basis. First of all, we need a real button to quit and stop using the entire application window for this. Additionally, we need a new top-level window where we can manage configuration settings. When the user changes settings, the application should verify changes and let the user know if something is wrong. To implement this, we need some basic UI elements.

A Button

The first thing is to create a button component, which will be used to quit the application, open the configuration window, close it, and so on. Our button should have basic visual parameters and send a signal when it is clicked. The button should also give some visual response that it has received user input. Certainly, a button can have many more features. There may be dozens of approaches for implementing a button, but we’ll just describe one which suits our needs.

Our button can be a simple click-sensitive rectangle with rounded corners. In previous sections, we saw that an element can receive mouse events if we include a MouseArea [http://qt-project.org/doc/qt-4.8/qml-mousearea.html] element and let it fill the entire surface of that element. We are going to use this approach for the button. Additionally, our button has to emit a signal notifying relevant parts of the application that it has been clicked. We need to use Qt Quick signals to implement this. Let’s take a look at how they work first.

We already got in touch with a related Qt Quick functionality when we saw that it is possible to implement a handler which reacts to property changes, for example, the status property of Image [http://qt-project.org/doc/qt-4.8/qml-image.html]:

Image {
    id: background
    source: "./content/resources/light_background.png"
    fillMode: "Tile"
    anchors.fill: parent
    onStatusChanged: if (background.status == Image.Error)
                         console.log (qsTr("Background image \"") +
                                      source +
                                      qsTr("\" cannot be loaded"))
}

Signals are very similar to the property notification changes. Signal handlers work the same, whereas they process a signal explicitly emitted in an item instead of a property change. Signal handlers can also receive signal parameters, which is not the case in property change handlers. Emitting a signal is a function call.

This is how it works for our Button component:

(src/utils/Button.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1
import "../js/style.js" as Style

Rectangle {
    id: root

    property string text: "Button"

    color: "transparent"

    width: label.width + 15
    height: label.height + 10

    border.width: Style.borderWidth
    border.color: pressedColor(Style.penColor)
    radius: Style.borderRadius

    signal clicked (variant mouse)
    signal pressedAtXY (string coordinates)

    function pressedColor (color) {
        return mouseArea.pressed ? Qt.darker(color, 5.0) : color
    }

    function logPresses (mouse) {
        pressedAtXY (mouse.x + "," + mouse.y)
    }

    Component.onCompleted: {
        mouseArea.clicked.connect(root.clicked)
    }

    Text {
        id: label
        anchors.centerIn: parent
        color: pressedColor(Style.penColor)
        text: parent.text
        font.pixelSize: Style.textPixelSize
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        Connections {
            onPressed: logPresses(mouse)
        }
        // this works as well instead of using Connections
        // onPressed: logPresses(mouse)
    }
}

Button defines two signals: clicked and pressedAtXY. We only use clicked in our application, and pressedAtXY has been added for demonstration purposes. Both signals are emitted in different ways. pressedAtXY is called from a JavaScript function called as an onPressed handler. clicked is connected directly to the clicked signal of the mouseArea item. Both ways have their own use cases. A direct signal-to-signal connection allows simple signal forwarding. This is what is needed in our Button, which should behave like a MouseArea when processing mouse events. In some other cases, you might have the need to add some additional processing before emitting a signal, like in the logPresses function.

A very important point to note here is the naming of signal parameters. If you take a look at the code for mouseArea above, you might wonder where the mouse parameter comes from. We did not declare it in our application. It actually belongs to the definition of clicked signal of the MouseArea [http://qt-project.org/doc/qt-4.8/qml-mousearea.html] element. The same happens with our pressedAtXY signal, which defines a coordinates parameter. All items using Button and processing the pressedAtXY signal has to access its parameter under the exact name. For example:

Button {
    id: toggleStatesButton
    ...
    onPressedAtXY: {
        console.log ("pressed at: " + coordinates)
    }
}

Note that we define the clicked signal as:

signal clicked (variant mouse)

We do this even though mouse is of the MouseEvent [http://qt-project.org/doc/qt-4.8/qml-mouseevent.html] type (according to the documentation for MouseArea [http://qt-project.org/doc/qt-4.8/qml-mousearea.html]). In the current version of Qt Quick, signal parameters can only be of basic types [http://qt-project.org/doc/qt-4.8/qdeclarativebasictypes.html]. This should not concern you as the type is converted to the appropriate type when it arrives.

For more details about using signals in Qt Quick, see the QML Signal and Handler Event System [http://qt-project.org/doc/qt-4.8/qmlevents.html] article in Qt documentation. You should also check the documentation of MouseArea [http://qt-project.org/doc/qt-4.8/qml-mousearea.html] as well as the QML Mouse Events [http://qt-project.org/doc/qt-4.8/mouseevents.html] article to discover more possibilities such as getting other mouse events, tracing hovering, and implementing drag-drop.

As any proper button, our Button should provide some visual feedback when it is clicked. We do this by tweaking its colors a bit. We have a small JavaScript function which modifies the color value to a new, pressed value. It makes the color darker in our case:

function pressedColor (color) {
    return mouseArea.pressed ? Qt.darker(color, 5.0) : color
}

We are going to toggle the color of the button border and of its label text. We bind the return value of this function to the border of the button:

border.color: pressedColor(Style.penColor)

Then we bind it to a color property of its label text:

color: pressedColor(Style.penColor)

That’s it! This is how our Button looks when unpressed:

../_images/button.png

and pressed:

../_images/button_pressed.png

A Simple Dialog

The Dialog is another utility component that we need. We use it to notify the user about critical situations. Our Dialog is very simple. It pops up on top of another element and just displays a text message that must be confirmed by clicking the OK button. This is the code for the new Dialog component:

(src/utils/Dialog.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1
import "../js/style.js" as Style

Rectangle {
    id: root

    property string message: "Error! This is a long message with details"

    width: 100
    height: 40

    color: Style.backgroundColor
    border.color: Style.penColor
    border.width: Style.borderWidth
    radius: Style.borderRadius

    visible: true

    function show(text) {
        root.message = text;
        root.visible = true;
    }

    function hide() {
        root.visible = false;
    }

    Text {
        id: messageText
        anchors.top: parent.top
        anchors.topMargin: Style.baseMargin
        anchors.left: parent.left
        anchors.right: parent.right
        horizontalAlignment: Text.AlignHCenter
        wrapMode: "WordWrap"
        text: root.message
        font.pixelSize: Style.textPixelSize
        color: Style.penColor
        onPaintedHeightChanged: {
            root.height = messageText.paintedHeight + okButton.height + 3*Style.baseMargin
        }
    }

    Button {
        id: okButton
        text: qsTr("OK")
        anchors.top: messageText.bottom
        anchors.topMargin: Style.baseMargin
        anchors.horizontalCenter: parent.horizontalCenter
        onClicked: root.hide()
    }
}

The Dialog is used by adding it as a child item to another element where it will pop-up from:

Item {
  id: root
  ...

  Dialog {
      id: errorDialog
      width: root.width
      anchors.centerIn: parent
      z: root.z+1
      visible: false
  }
  ...

  Button {
    id: exitButton
    ...
    onClicked: {
      ...
      errorDialog.show (qsTr("The location cannot be empty"));
      ...
    }
  }
  ...
}

When loaded, the Dialog initially stays invisible. It goes on top of its parent (root in the code segment above). z: root.z+1 does this trick. We bind its z property to a value which is always higher than the value of root.z. Later, we call show with a message to be displayed. show makes the Dialog visible and stores the message text to be displayed. When the user clicks the OK button, the Dialog hides itself again.

Note

The TabWidget Example [http://qt-project.org/doc/qt-4.8/declarative-ui-components-tabwidget.html] in Qt documentation shows another approach toward dynamically showing and hiding elements on top of others.

Our Dialog has a few other features which are useful to know. In order to use the screen space efficiently, it copies the width from its parent. We also set the messageText property, wrapMode to the WordWrap value. When the Dialog opens with a long message text, the message wraps it to the Dialog width. The messageText element changes the height of the root Dialog when its height has changed due to wrapping:

Rectangle {
  id: root
  ...
  Text {
    id: messageText
    ...
    onPaintedHeightChanged: {
      root.height = messageText.paintedHeight +
                    okButton.height +
                    3    Style.baseMargin
    }
  ...
}

This is how it looks on the screen:

../_images/dialog.png

A Checkbox

We can use the Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] element to get text or digit based user input, but we need something else for on-off type of settings. Usually, this is done using the checkbox UI elements. There is no checkbox element in Qt Quick, and we are going to make it from scratch. It is not a problem at all as we can easily create one using Qt Quick. This is the complete code for our new CheckBox component:

(src/utils/CheckBox.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1

Item {
    id: root
    property bool checked: true
    // we should pre-set the size to get it working perperly in a positioner
    width: checkBox.width
    height: checkBox.height

    Image {
        id: checkBox
        source: root.checked ?
                    "../content/resources/checkbox.png" :
                    "../content/resources/draw-rectangle.png"
        Keys.onPressed: {
            if (event.key == Qt.Key_Return ||
                    event.key == Qt.Key_Enter ||
                    event.key == Qt.Key_Space)
                root.checked = !root.checked;
        }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                root.checked = !root.checked;
           }
        }
    }
    //onValueChanged: console.log ("value: " + root.value)
}

Our CheckBox is based on Item [http://qt-project.org/doc/qt-4.8/qml-item.html]. It extends it just by one boolean property called checked. If the box is checked, checked is true. Otherwise it is false. The entire visual implementation of the CheckBox consists of two images which are flipped back and forth. This is done by binding the source property of the checkBox Image [http://qt-project.org/doc/qt-4.8/qml-image.html] item to a checkbox image or to an image of a normal rectangle depending on the checked property.

This is how our CheckBox looks on the screen when checked and unchecked:

../_images/checkbox_checked.png ../_images/checkbox_unchecked.png

Further on, there is a section of code, which includes the keyboard navigation handling. This topic will be discussed in the next section.

Handling Keyboard Input and Navigation

Another important aspect of interaction with the user is handling the keyboard input and navigation. We will explore this while walking through the implementation of the Configure component based on the new UI components we introduced earlier.

Meanwhile, we have several hard-coded property values which actually should be changeable by the user:

  • Location name for the weather forecast
  • Time interval in which the weather data should be updated
  • Turning off the seconds and date display to make the clock more compact

The name and interval properties require a text input field, whereas the last one can be implemented using the Checkbox.

Text input is straightforward: Qt Quick provides the Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] element for this. We use it to get a new value for the forecast location and new value for the forecast update interval. The Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] element binds the captured keyboard input to the text property. When we load this element, we preset according to text properties with the locationTextInput and forecastUpdateInterval values to display the current settings to the user. Users can start editing and we do not need to take care of any details for text input handling:

...
TextInput {
    id: locationTextInput
    ...
    width: controlElements.width - locationTextInput.x - controlElements.spacing
    text: locationText
    focus: true
}
...
TextInput {
    id: updateTextInput
    ...
    text: forecastUpdateInterval
    maximumLength: 3
    // we use IntValidator just to filter the input. onAccepted is not used here
    validator: IntValidator{bottom: 1; top: 999;}
}
...

The code above has a few things on top.

updateTextInput uses a validator to limit the length of the text and ensure that we get digits in a proper range.

Location names do not need a validator, but they need something to handle text input which is longer than just a few digits in updateTextInput. This can be achieved limiting the width to ensure that a long text does not leave the boundaries of the top-level item. If we do not do this and keep width undefined, Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] will expand, follow the entered text and at some time leave the visual boundaries.

Note

If you have a multi-line text that needs to be edited by the user, you can use the Text Edit [http://qt-project.org/doc/qt-4.8/qml-textedit.html] element.

locationTextInput receives the keyboard focus explicitly, because we set focus to true. When Configure is loaded, the user can start changing the location name:

../_images/configure.png

The elements Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] and our new CheckBoxes react to mouse clicks. How can the user navigate from one input element to another if we would like to support navigation with keyboard in addition to the mouse? What should we do if we need to enable keyboard input in CheckBoxes as well? .

Qt Quick provides key navigation and raw key processing for these cases. Let’s take a look at key navigation first.

These are the changes in the code for our two Text Input [http://qt-project.org/doc/qt-4.8/qml-textinput.html] elements to support key navigation:

TextInput {
    id: locationTextInput
    ...
    focus: true
    KeyNavigation.up: offlineCheckBox
    KeyNavigation.down: updateTextInput
}
...
TextInput {
    id: updateTextInput
    ...
    KeyNavigation.up: locationTextInput
    KeyNavigation.down: secondsCheckBox
}

locationTextInput explicitly pulls the focus by setting its focus property to ``true. The Key Navigation [http://qt-project.org/doc/qt-4.8/qml-keynavigation.html] items provide attached properties, which monitor key presses and move of the input focus from one element to another. Key Navigation [http://qt-project.org/doc/qt-4.8/qml-keynavigation.html] is a big help in our case where we have many elements and need to organize the movement of the input focus in a certain way.

In the code sample above, the input focus is moved from the locationTextInput item to the updateTextInput item if the down* arrow key is pressed. The focus goes back from updateTextInput to locationTextInput if the user presses the up key and so on. We add such statements to all relevant elements in the Configure component.

While processing user input, you sometimes need to capture particular keys. This is the case with our Checkboxes. Working with desktop applications, users have learned that it is possible to toggle a check box with the space key*. We should implement this feature in our application.

This is where the Keys [http://qt-project.org/doc/qt-4.8/qml-keys.html] items can be used. It is basically a kind of signal sender for almost every key on the keyboard. Its signals have KeyEvent [http://qt-project.org/doc/qt-4.8/qml-keyevent.html] as a parameter, and it contains detailed information about the key pressed. We use Keys [http://qt-project.org/doc/qt-4.8/qml-keys.html] in our checkboxes. The code segment in the previous section uses the attached Keys.onPressed property, which toggles the Checkbox state on Return, Enter and Space keys.

More details about keyboard input processing is available in the Keyboard Focus in QML [http://qt-project.org/doc/qt-4.8/qdeclarativefocus.html] article in Qt Documentation.

By now we have got all input elements and can process user input. One step is still needed to finish our Configure component. This is a verification and storing of the new values.

When the user clicks the exitButton, we need to check the new setting values and pass them to the application if they are ok. This is also a place where we use our Dialog to inform the user that the new values are not OK if needed. In this case, the Configure does not close and stays open until the user provides the correct values. See the onClicked handler code for exitButton to learn how this is achieved:

(src/components/Configure.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1
import "../utils" 1.0
import "../js/style.js" as Style

Rectangle {
    id: root
    property bool showSeconds: true
    property bool showDate: true
    property int forecastUpdateInterval: 5 // minutes
    property string locationText: "Munich"
    property bool forceOffline: false

    width: 320
    height: 480

    Image {
        id: background
        source: Style.backgroundImage
        fillMode: "Tile"
        anchors.fill: parent
    }

    Grid {
        id: controlElements
        spacing: 10
        columns: 2
        anchors.left: root.left
        anchors.leftMargin: spacing
        anchors.verticalCenter: root.verticalCenter
        anchors.right: root.right

        Text {
            id: locationLabel
            text: qsTr("Forecast for:<br>(city name)")
            color: locationTextInput.focus?
                       Qt.lighter(Style.penColor) : Style.penColor
            font.pixelSize: Style.textPixelSize
        }

        TextInput {
            id: locationTextInput
            width: controlElements.width - locationTextInput.x - controlElements.spacing
            text: locationText
            font.pixelSize: Style.textPixelSize
            color: Style.penColor
            focus: true
            KeyNavigation.up: offlineCheckBox
            KeyNavigation.down: updateTextInput
        }

        Text {
            id: updateLabel
            height: 90
            text: qsTr("update interval: <br>(in min)")
            color: updateTextInput.focus?
                       Qt.lighter(Style.penColor) : Style.penColor
            font.pixelSize: Style.textPixelSize
        }

        TextInput {
            id: updateTextInput
            width: locationTextInput.width
            text: forecastUpdateInterval
            font.pixelSize: Style.textPixelSize
            color: Style.penColor
            maximumLength: 3
            // we use IntValidator just to filter the input
            // onAccepted is not used here
            validator: IntValidator{bottom: 1; top: 999;}
            KeyNavigation.up: locationTextInput
            KeyNavigation.down: secondsCheckBox
        }

        Text {
            id: secondsLabel
            text: qsTr("Show seconds:")
            color: secondsCheckBox.focus?
                       Qt.lighter(Style.penColor) : Style.penColor
            font.pixelSize: Style.textPixelSize
        }

        CheckBox {
            id: secondsCheckBox
            checked: showSeconds
            KeyNavigation.up: updateTextInput
            KeyNavigation.down: dateCheckBox
        }

        Text {
            id: dateLabel
            text: qsTr("Show date:")
            color: dateCheckBox.focus?
                       Qt.lighter(Style.penColor) : Style.penColor
            font.pixelSize: Style.textPixelSize
        }

        CheckBox {
            id: dateCheckBox
            checked: showDate
            KeyNavigation.up: secondsCheckBox
            KeyNavigation.down: offlineCheckBox
        }

        Text {
            id: offlineLabel
            text: qsTr("Clock only")
            color: offlineCheckBox.focus?
                       Qt.lighter(Style.penColor) : Style.penColor
            font.pixelSize: Style.textPixelSize
        }

        CheckBox {
            id: offlineCheckBox
            checked: forceOffline
            KeyNavigation.up: secondsCheckBox
            KeyNavigation.down: locationTextInput
        }
    }

    Dialog {
        id: errorDialog
        width: root.width
        anchors.centerIn: parent
        z: root.z+1
        visible: false
    }

    Button {
        id: exitButton
        text: qsTr("OK")
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 10
        onClicked: {
            // update interval and location cannot be empty
            // update interval cannot be zero
            if (updateTextInput.text == "" || updateTextInput.text == 0)
                errorDialog.show (qsTr("The update interval cannot be empty"))
            else if (locationTextInput.text == "")
                errorDialog.show (qsTr("The location cannot be empty"))
            else {
                forecastUpdateInterval = updateTextInput.text
                root.locationText = locationTextInput.text
                root.visible = false
            }
            // update check box relevant settings
            root.showSeconds = secondsCheckBox.checked
            root.showDate = dateCheckBox.checked
            root.forceOffline = offlineCheckBox.checked
        }
    }
}

What’s Next?

You’ve probably noticed that the offlineCheckBox item with the magical forceOffline setting associated with it. This setting is new. It is used to toggle the states in the next version of our application which will be the topic for the next chapter. We also will take a look at animations in Qt Quick and use them to implement some nice effects in the final version of our application.