Time Picker in QML

Matthias Kuhn picture Matthias Kuhn · Mar 26, 2015 · Viewed 10.2k times · Source

I need to give the user the possibility to select a date and time within a QML application. For selection dates there is the Calendar in QtQuick Controls. I haven't found a similar control to let the user select the time of day.

On the internet there are a couple of examples like Grog or Harmattan. I assume however that they do not integrate with the native look and feel like the other QtQuick Controls do.

Is there a standard approach which I am not aware of, good alternatives I have not come across or recommendations about which to choose?

Answer

BaCaRoZzo picture BaCaRoZzo · Mar 28, 2015

Since Qt 5.5 the so called Qt Quick Enterprise Controls will be available also in the community edition of Qt under the name Qt Quick Extras. Among the others, the Tumbler seems a feasible solution for your requirements: you can easily setup two columns, one for the hours and one for the minutes.

If you are still interested in the circular selection (or wants to implement your own tumbler) you can take different routes such as create your own component inheriting from QQuickItem or QQuickPaintedItem or exploiting a custom view with PathView. The latter is the case I'm going to cover in this answer. Just refer to the provided links for examples about custom components creation.

Citing the documentation of PathView:

The view has a model, which defines the data to be displayed, and a delegate, which defines how the data should be displayed. The delegate is instantiated for each item on the path. The items may be flicked to move them along the path.

Hence the path defines the way items are laid out on the screen, even in a circular fashion. A path can be constructed via a Path type, i.e. a sequence of path segments of different kind. PathArc is the one we are interested in, since it provides the desired rounded shape.

The following example uses these elements to define a circular time picker. Each path is constructed by exploiting the currentIndexof the delegate: an integer is used as model for the PathViews - 12 for the hours view and 6 for the minutes view, respectively. The text of the delegates is generated by exploiting the index attached property and manipulating it to generate hours and 10-minutes interval values (see the delegates Text items). Finally, the text of the current element (i.e. the currentItem) is bound to the time label in the center of the window: as the currentIndex and currentItem change, also the label gets updated.

The overall component looks like this:

enter image description here

highlightcomponents (blue and green circles) are used to graphically representing editing of the time: when visible the time can be edited, i.e. another Item of the path can be selected. Switching between normal and editing mode occurs by clicking the time label in the center.

When in editing mode the user can simply hover the different hours/minutes values to select them. If the newly selected hour/minute is clicked the editing for that specific PathView is disabled and the corresponding highlight circle disappears.

This code is clearly just a toy example to give you a grasp of what PathView can be used for. Several improvements can be done, e.g. animations, a better number positioning, detailed minutes representation, a nice background and so on. However they are out of scope w.r.t. the question and were not considered.

import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Controls.Styles 1.3
import QtQuick.Layouts 1.1

Window {
    visible: true
    width: 280; height: 280

    RowLayout {             // centre time label
        anchors.centerIn: parent
        Text {
            id: h
            font.pixelSize: 30
            font.bold: true
            text: outer.currentItem.text
        }
        Text {
            id: div
            font.pixelSize: 30
            font.bold: true
            text: qsTr(":")
        }
        Text {
            id: m
            font.pixelSize: 30
            font.bold: true
            text: inner.currentItem.text
        }

        MouseArea {
            anchors.fill: parent
            onClicked: outer.choiceActive = inner.choiceActive = !outer.choiceActive
        }
    }


    PathView {          // hours path
        id: outer
        property bool pressed: false
        model: 12

        interactive: false
        highlightRangeMode:  PathView.NoHighlightRange
        property bool choiceActive: false

        highlight: Rectangle {
            id: rect
            width: 30 * 1.5
            height: width
            radius: width / 2
            border.color: "darkgray"
            color: "steelblue"
            visible: outer.choiceActive
        }

        delegate: Item {
            id: del
            width: 30
            height: 30
            property bool currentItem: PathView.view.currentIndex == index
            property alias text : textHou.text
            Text {
                id: textHou
                anchors.centerIn: parent
                font.pixelSize: 24
                font.bold: currentItem
                text: index + 1
                color: currentItem ? "black" : "gray"
            }

            MouseArea {
                anchors.fill: parent
                enabled: outer.choiceActive
                onClicked: outer.choiceActive = false
                hoverEnabled: true
                onEntered: outer.currentIndex = index
            }
        }

        path: Path {
            startX: 200; startY: 40
            PathArc {
                x: 80; y: 240
                radiusX: 110; radiusY: 110
                useLargeArc: false
            }
            PathArc {
                x: 200; y: 40
                radiusX: 110; radiusY: 110
                useLargeArc: false
            }
        }
    }

    PathView {          // minutes path
        id: inner
        property bool pressed: false
        model: 6
        interactive: false
        highlightRangeMode:  PathView.NoHighlightRange
        property bool choiceActive: false

        highlight: Rectangle {
            width: 30 * 1.5
            height: width
            radius: width / 2
            border.color: "darkgray"
            color: "lightgreen"
            visible: inner.choiceActive
        }

        delegate: Item {
            width: 30
            height: 30
            property bool currentItem: PathView.view.currentIndex == index
            property alias text : textMin.text
            Text {
                id: textMin
                anchors.centerIn: parent
                font.pixelSize: 24
                font.bold: currentItem
                text: index * 10
                color: currentItem ? "black" : "gray"
            }

            MouseArea {
                anchors.fill: parent
                enabled: inner.choiceActive
                onClicked: inner.choiceActive = false
                hoverEnabled: true
                onEntered: inner.currentIndex = index
            }
        }

        path: Path {
            startX: 140; startY: 60
            PathArc {
                x: 140; y: 220
                radiusX: 40; radiusY: 40
                useLargeArc: false
            }
            PathArc {
                x: 140; y: 60
                radiusX: 40; radiusY: 40
                useLargeArc: false
            }
        }
    }

    // to set current time!
    onVisibleChanged: {
        var d = new Date();
        outer.currentIndex = d.getUTCHours() % 12
        inner.currentIndex = d.getMinutes() / 10
    }
}