Kyle Strand
Kyle Strand

Reputation: 16499

Simple keyboardless touchscreen widgets in Qt

I'm looking for a simple way to make widgets for a touch-screen that will allow users to set the time and IP address on the computer running the code and provide a simple (uppercase Latin-alphabetic) name.

This question is not about how to actually set the system time or IP address; I'm just looking for information about how to make the graphical widgets themselves.

What I want is for each editable property (time, address, and name) to be divided into "scrollable" fields, where the fields for "time" are hours, minutes, possibly seconds, and AM/PM/24-hr, and the fields for address/name are the individual characters. Each field would have an arrow above and below it, and touching on an arrow would scroll through the valid values for that field.

I think this is a pretty common UX pattern, especially in meatspace (e.g. on alarm clocks), but just in case it's not clear what I'm trying to describe, here's an example with a user editing the "name" property:

^^^
BN
vvv

User presses "down" below the "N": ^^^ BO vvv

User presses "down" below the empty space:

^^^^
BOA
vvvv

...and again on the same down-arrow:

^^^^
BOB
vvvv

I'm writing this using C++14 with Qt 5. (If worst comes to worst, I'd be open to writing a separate app using a different language and/or framework, but I'm not asking for framework suggestions here; if you have one, let me know and I'll open a corresponding question on Software Recommendations SE.)

I don't see anything in the Qt 5 widget library like this; most of the input widgets are text fields. QSpinBox looks somewhat promising, but the arrows are probably too small for my touchscreen, and using a separate spinbox for each letter would probably be confusing and ugly.

I don't really know enough about Qt or GUI-programming in general to feel confident trying to write my own widgets from scratch, but this interface looks simple enough that I would expect a couple lines of QML would get me well on my way.

Upvotes: 0

Views: 900

Answers (1)

BaCaRoZzo
BaCaRoZzo

Reputation: 7692

ListView as well as PathView can produce the desired result with slightly different behaviors and slightly different performances. Differently from ListView, PathView is circular, i.e. elements can be iterated continuously by using just one of the selection controls. It is also easier to fully customize the behavior of the path in PathView via the PathAttribute type. Anyhow path customization seems not to be a required feature, according to the question.

If you implement the solution via a ListView you should ensure that just one element is shown and that any model is processed.

Component {
    id: spinnnnnnnner

    Column  {
        width: 100
        height: 110
        property alias model: list.model
        property string textRole: ''
        spacing: 10

        Item {
            width: 100
            height: 25
            Text { anchors.centerIn: parent; text: "-"; font.pixelSize: 25; font.bold: true }
            MouseArea {anchors.fill: parent; onClicked: list.decrementCurrentIndex() }
        }

        ListView {
            id: list
            clip: true
            width: 100
            height: 55
            enabled: false          // <--- remove to activate mouse/touch grab
            highlightRangeMode: ListView.StrictlyEnforceRange   // <--- ensures that ListView shows current item

            delegate: Text {
                width: ListView.view.width
                horizontalAlignment: Text.AlignHCenter
                font.pixelSize: 50
                font.bold: true
                text: textRole === "" ? modelData :
                                        ((list.model.constructor === Array ? modelData[textRole] : model[textRole]) || "")
            }
        }

        Item {
            width: 100
            height: 25
            Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 25; font.bold: true }
            MouseArea {anchors.fill: parent; onClicked: list.incrementCurrentIndex() }
        }
    }
}

The checks over the model ensure that any type of model can be passed to the component. Here is an example using three very different models:

import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1

ApplicationWindow {
    visible: true

    width: 400
    height: 300

    ListModel {
        id: mod
        ListElement {texty: "it1"}
        ListElement {texty: "it2"}
        ListElement {texty: "it3"}
    }

    Row {
        Repeater {
            id: rep
            model: 3
            delegate: spinnnnnnnner

            Component.onCompleted: {
                rep.itemAt(0).model = mod                       // listmodel
                rep.itemAt(0).textRole = "texty"
                rep.itemAt(1).model = 10                        // number model
                //
                rep.itemAt(2).model = ["foo", "bar", "baz"]     // array model
            }
        }
    }
}

PathView implementation is not so different from the ListView one. In this case it is sufficient to define a vertical path and specify that just one one element is visible at a time via pathItemCount. Finally, setting preferredHighlightBegin/preferredHighlightEnd ensures that the visible element is centered in the view. The revisited component is the following:

Component {
    id: spinnnnnnnner

    Column  {
        width: 100
        height: 110
        property alias model: list.model
        property string textRole: ''
        spacing: 10

        Item {
            width: 100
            height: 25
            Text { anchors.centerIn: parent; text: "-"; font.pixelSize: 25; font.bold: true }
            MouseArea {anchors.fill: parent; onClicked: list.decrementCurrentIndex() }
        }

        PathView {
            id: list
            clip: true
            width: 100
            height: 55
            enabled: false          // <--- remove to activate mouse/touch grab
            pathItemCount: 1
            preferredHighlightBegin: 0.5
            preferredHighlightEnd: 0.5

            path: Path {
                startX: list.width / 2; startY: 0
                PathLine { x: list.width / 2; y: list.height  }
            }

            delegate: Text {
                width: PathView.view.width
                horizontalAlignment: Text.AlignHCenter
                font.pixelSize: 50
                font.bold: true
                text: textRole === "" ? modelData :
                                        ((list.model.constructor === Array ? modelData[textRole] : model[textRole]) || "")
            }
        }

        Item {
            width: 100
            height: 25
            Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 25; font.bold: true }
            MouseArea {anchors.fill: parent; onClicked: list.incrementCurrentIndex() }
        }
    }
}

Upvotes: 1

Related Questions