Выбор времени в QML

Мне нужно дать пользователю возможность выбрать дату и время в приложении QML. Для дат выбора есть Calendar в элементах управления QtQuick. Я не нашел аналогичного элемента управления, чтобы пользователь мог выбрать время суток.

В Интернете есть несколько примеров, таких как Grog или Harmattan. Я предполагаю, однако, что они не интегрируются с внешним видом и чувствуют, что другие QtQuick Controls делают.

Есть ли стандартный подход, о котором я не знаю, хорошие альтернативы, с которыми я не сталкивался, или рекомендации по выбору?

Ответ 1

Так как Qt 5.5, так называемый Qt Quick Enterprise Controls будет доступен также в издании сообщества Qt под названием Qt Quick Extras. Среди прочих, Tumbler кажется приемлемым решением для ваших требований: вы можете легко настроить два столбца: один для часов и один для мин.

Если вы по-прежнему интересуетесь круговым выбором (или хотите реализовать свой собственный тумблер), вы можете использовать разные маршруты, например, создать свой собственный компонент, наследующий от QQuickItem или QQuickPaintedItem или используя пользовательское представление PathView. Последний случай я рассмотрю в этом ответе. Для примера о создании пользовательских компонентов обратитесь к приведенным ссылкам.

Ссылаясь на документацию PathView:

В представлении есть модель, которая определяет отображаемые данные, и делегат, который определяет, как данные должны отображаться. Делегат создается для каждого элемента на пути. Элементы могут перемещаться по пути.

Следовательно, путь определяет способ размещения элементов на экране, даже круговым способом. Путь может быть построен с помощью типа Path, т.е. Последовательности сегментов пути разного типа. PathArc - это тот, который нас интересует, поскольку он обеспечивает желаемую округлую форму.

В следующем примере эти элементы используются для определения кругового выбора времени. Каждый путь строится путем использования currentIndex делегата: целое число используется как модель для PathView - 12 для представления часов и 6 для представления минут соответственно. Текст делегатов генерируется путем использования прикрепленного свойства index и манипулирования им для генерации часов и 10-минутных интервальных значений (см. Делегаты Text). Наконец, текст текущего элемента (т.е. currentItem) привязан к метке времени в центре окна: при изменении currentIndex и currentItem также обновляется метка.

Общий компонент выглядит следующим образом:

enter image description here

highlight компоненты (синие и зеленые круги) используются для графического представления времени редактирования: при видимом времени можно редактировать время, т.е. можно выбрать другой Item пути. Переключение между обычным и режимом редактирования происходит путем нажатия метки времени в центре.

В режиме редактирования пользователь может просто навести различные значения часов/минут, чтобы выбрать их. Если щелкнуть вновь выбранный час/минуту, редактирование для этого конкретного PathView отключено, и соответствующий кружок подсветки исчезнет.

Этот код - это просто пример игрушек, который поможет вам понять, для чего можно использовать PathView. Можно сделать несколько улучшений, например. анимации, лучшее позиционирование чисел, подробное представление минут, хороший фон и так далее. Однако они выходят за рамки w.r.t. вопрос и не рассматривался.

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
    }
}

Ответ 2

я думаю, что мое собственное средство выбора времени хорошо, вы можете расширить его так, как вам нравится в персидской ориентации, вам нужно немного поменяться местами или использовать какое-либо зеркальное отображение макетов:
UButton.qml

import QtQuick 2.4
import QtQuick.Controls 2.4
import QtQuick.Controls.Universal 2.4
Button {
    id:root
    Universal.accent: Universal.Cobalt
    Universal.foreground: "white"
    highlighted: true
    font.family: "B Nazanin"
    font.pointSize: 12
}

UCard.qml

import QtQuick 2.4
import QtQuick.Controls 2.4
import QtQuick.Controls.Universal 2.4
import QtGraphicalEffects 1.0
Item{
    property alias radius : morakhasiRect.radius
    property alias color : morakhasiRect.color
    implicitWidth: 150
    implicitHeight: 150

    Rectangle{
        anchors.rightMargin: 1
        anchors.leftMargin: 1
        anchors.bottomMargin: 1
        anchors.topMargin: 1
        id:morakhasiRect
        anchors.fill: parent
        color: "#f5f5f5"
    }
    DropShadow {
        anchors.fill: morakhasiRect
        radius: 9.0
        samples: 17
        color: "#80000000"
        source: morakhasiRect
    }
}

URect.qml

Rectangle{
    color: "transparent"
    border.color: Universal.color(Universal.Cobalt)
    border.width: 1
}

UTumbler.qml

import QtQuick 2.0
import QtQuick.Controls.Universal 2.4
import QtQuick.Controls 2.4
Tumbler{
    id:hourSpin
    wrap: false
    delegate: Text{

        font.pointSize: 12
        text: modelData
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        opacity: 1.0 - Math.abs(Tumbler.displacement) / (hourSpin.visibleItemCount / 2)
    }
    Rectangle {
        anchors.horizontalCenter: hourSpin.horizontalCenter
        y: hourSpin.height * 0.4
        width: 40
        height: 1
        color: Universal.color(Universal.Cobalt)
    }

    Rectangle {
        anchors.horizontalCenter: hourSpin.horizontalCenter
        y: hourSpin.height * 0.6
        width: 40
        height: 1
        color: Universal.color(Universal.Cobalt)
    }
}

UTimeDialog

import QtQuick 2.0
import QtQuick.Controls 2.4
import QtQuick.Controls.Universal 2.4
Item{
    id:root
    property alias hour : hourSpin.currentIndex
    property alias minute : minuteSpin.currentIndex
    signal open
    signal close
    signal accepted
    signal rejected
    visible: element.opened
    onOpen: element.open()
    onClose: element.close()
    implicitWidth: 200
    implicitHeight: 200
    Dialog {
        id: element
        modal: true
        width: parent.width
        height: parent.height
        padding: 5
        margins: 5
        background: Item{

        }

        onAccepted: {
            root.accepted()
        }
        onRejected: {
            root.rejected()
        }
        contentItem: UCard{
            anchors.fill: parent
            radius: 10
        }

        Column{
            id: column
            spacing: 30
            anchors.centerIn: parent
            Row{
                id: row
                spacing: 20
                anchors.horizontalCenter: parent.horizontalCenter
                Column{
                    id: column1
                    spacing: 15
                    height: 80
                    width: 50
                    clip:true

                    UTumbler{
                        id:hourSpin
                        anchors.horizontalCenter: parent.horizontalCenter
                        anchors.verticalCenter: parent.verticalCenter
                        model: 24
                    }

                }
                Text{
                    text: ":"
                    font.pointSize: 12
                    anchors.verticalCenter: parent.verticalCenter
                    font.family: "B Nazanin"
                }
                Column{
                    id: column2
                    spacing: 15
                    height: 80
                    width: 50
                    clip:true

                    UTumbler{
                        id:minuteSpin
                        anchors.horizontalCenter: parent.horizontalCenter
                        anchors.verticalCenter: parent.verticalCenter
                        model: 60
                    }

                }

            }
            Row{
                anchors.horizontalCenter: parent.horizontalCenter
                spacing: 40
                UButton{
                    text:"select"

                    onClicked: {
                        element.reject()
                    }
                }
                UButton{
                    text: "cancel"
                    onClicked: {
                        element.accept()
                    }
                }
            }
        }

    }
}

UIcoButton.qml

import QtQuick 2.4
import QtQuick.Controls 2.4
import QtQuick.Controls.Universal 2.4
import QtGraphicalEffects 1.0
FocusScope{
    id:focusScope
    signal clicked
    property alias font : icoText.font.family
    property alias icon : icoText.text
    property alias size : icoText.font.pixelSize
    property alias caption : captionTxt.text
    property alias spacing : row.spacing
    property string colorEnter :Universal.color(Universal.Cobalt)
    property string colorExit :"#00171f"
    property alias state: root.state
    implicitWidth: captionTxt.text!= "" ? 100 : 35
    implicitHeight: 40
    Rectangle {
        id: root
        radius: 0
        anchors.fill: parent
        color: colorExit
        state: "default"
        focus: true

        onFocusChanged: {
            if(focus){
                root.border.width = 1
                root.border.color = Universal.color( Universal.Cobalt)
            }
            else{
                root.border.width = 0
                root.border.color = "transparent"
            }
        }

        Row{
            id: row
            anchors.rightMargin: 5
            anchors.leftMargin: 5
            anchors.bottomMargin: 5
            anchors.topMargin: 5
            anchors.fill: parent
            layoutDirection: Qt.RightToLeft
            spacing: 15
            Text {
                id: icoText
                text: ""
                anchors.verticalCenter: parent.verticalCenter
                font.pixelSize: 25
                font.family: "fontawesome"
                color: "white"
            }
            Text{
                id:captionTxt
                text: ""
                anchors.verticalCenter: parent.verticalCenter
                font.pixelSize: icoText.font.pixelSize * 55 /100
                font.family: "B Nazanin"
                color: "white"
                visible: text!= ""
            }

        }
        InnerShadow {
            id:shadow
            anchors.fill: row
            radius: 1.0
            samples: 17
            horizontalOffset: 1
            color: colorExit
            source: row
            visible: false
        }
        //    Glow {
        //        id:shadow
        //        anchors.fill: row
        //        radius: 6
        //        samples: 25
        //        color: "white"
        //        source: row
        //        visible: false
        //    }

        MouseArea{
            id: mouseArea
            anchors.fill: parent
            hoverEnabled: true
            onEntered: {
                if(root.state == "default")
                    root.color = colorEnter
                else{
                    icoText.color = colorEnter
                    captionTxt.color = colorEnter
                }
            }

            onExited: {
                if(root.state == "default")
                    root.color = colorExit
                else{
                    icoText.color = colorExit
                    captionTxt.color = colorExit
                }
            }

            onPressed: {
                shadow.visible = true
            }

            onReleased: {
                shadow.visible = false
            }

            onClicked: {
                focusScope.clicked()
            }
        }
        states: [
            State {
                name: "transparent"
                PropertyChanges {
                    target: root
                    color:"transparent"
                }
                PropertyChanges {
                    target: icoText
                    color:colorExit
                }
                PropertyChanges {
                    target: captionTxt
                    color:colorExit
                }
            },
            State{
                name: "default"
                PropertyChanges {
                    target: root
                    color:"#00171f"
                }
                PropertyChanges {
                    target: icoText
                    color:"white"
                }
                PropertyChanges {
                    target: captionTxt
                    color:"white"
                }
            }

        ]
    }
}

UTimePicker

import QtQuick 2.4
import QtQuick.Controls 2.4
import QtQuick.Controls.Universal 2.4
Item {
    id: scope
    clip: true
    QtObject{
        id:variables
        property var time: ({hour: 0, minute: 0})
        onTimeChanged: {
            refreshDialogTime()

        }
    }
    signal changed
    property alias caption : captionTxt.text
    property size size : Qt.size(30,70)
    property string splitter : ":"
    property alias spacing : row.spacing
    Component.onCompleted: {
        var q = new Date()
        var curtime = q.toLocaleTimeString().substring(0,5);
        if(splitter != ":"){
            curtime.replace(':',splitter)
        }
        var vars = curtime.split(':')
        setTime(vars[0],vars[1])
        refreshDialogTime()
    }
    function refreshDialogTime(){
        dialog.hour = variables.time.hour
        dialog.minute = variables.time.minute
    }

    function getTime(){
        return variables.time;
    }

    function setTimeString(time){
        textArea.text= time
    }

    function setTime(hour,minute){
        var _hour = hour
        if(_hour<10){
            _hour = "0"+hour.toString()
        }
        else{
            _hour = hour.toString()
        }
        var _minute = minute
        if(_minute <10){
            _minute = "0"+minute.toString()
        }
        else{
            _minute = minute.toString()
        }

        var time = _hour+":"+_minute
        textArea.text = time
    }
    implicitHeight: 50
    implicitWidth: 200
    Row{
        id: row
        width: parent.width
        height: parent.height
        spacing: 25
        layoutDirection: Qt.RightToLeft
        Text{
            font.bold: true
            id: captionTxt
            font.pointSize: 12
            horizontalAlignment: Text.AlignRight
            anchors.verticalCenter: parent.verticalCenter
            width: scope.size.width * scope.width /100 - scope.spacing/2
            verticalAlignment: Text.AlignVCenter
            font.family: "B Nazanin"

        }
        Item{
            id: element
            anchors.verticalCenter: parent.verticalCenter
            height: parent.height
            width: scope.size.height * scope.width /100 - scope.spacing/2
            Rectangle{
                id:backrec
                height: parent.height
                anchors.verticalCenter: parent.verticalCenter
                width: parent.width
                border.width: 1
                border.color: "black"
                TextField{
                    id:textArea
                    selectByMouse: true

                    anchors.verticalCenter: parent.verticalCenter

                    height: parent.height
                    rightPadding: 5
                    bottomPadding: 5
                    topPadding: 5
                    padding: 5
                    verticalAlignment: Text.AlignVCenter
                    onFocusChanged: {
                        if(focus){
                            captionTxt.color = Universal.color( Universal.Cobalt)
                            backrec.border.color = Universal.color( Universal.Cobalt)

                        }
                        else{
                            captionTxt.color = "black"
                            backrec.border.color = "black"
                        }
                    }
                    background: URect{

                        color: "transparent"
                        border.color: "black"
                        border.width: 0
                    }
                    onTextChanged: {
                        var _temp = text.split(splitter)
                        if(_temp.length>0){

                            variables.time.hour =_temp[0]==""?0:  _temp[0]
                            variables.time.minute = _temp[1]==""?0:_temp[1]
                        }
                        changed()
                    }


                    placeholderText : "HH:mm"
                    anchors.right: parent.right
                    anchors.left: iconBtn.right
                    font.family: "B Nazanin"
                    font.pointSize: 12
                    inputMask:  "99:99"
                    validator: RegExpValidator { regExp: /^([0-1\s]?[0-9\s]|2[0-3\s]):([0-5\s][0-9\s])$ / }
                }
                IcoButton{
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.left: parent.left
                    anchors.leftMargin: 2
                    id:iconBtn
                    caption: ""
                    size: 30
                    icon: "\uf017"
                    state: "transparent"
                    onClicked: {
                        textArea.focus = true
                        dialog.open()
                    }
                }
            }


        }
    }
    UTimeDialog{
        id:dialog
        x:iconBtn.x
        y:iconBtn.y+ scope.height
        onAccepted: {
            setTime(hour,minute)
        }
    }
}

пример

UTimePicker{
    x: 285
    width: 200
    spacing: 15
    size: Qt.size(35,65)
    caption: "time"
    onChanged: {
        var i =  getTime()
        console.log(i.hour)
        console.log(i.minute)
    }
}

который выглядит так: ScreenShot
ScreenShot2

для зеркалирования:

    LayoutMirroring.enabled: true
    LayoutMirroring.childrenInherit: true

если кто-то заинтересовался, я мог бы поделиться библиотекой для этого