Menu
Onze expertiseHigh-end HMI's & user interfacesEngineering & test toolsHoe we helpenSectorenAutomotiveConsumer electronicsIndustrial and toolsMedicalTechnologyOver onsCasesInzichtenContact
QML

Hoe bouw je een virtueel QML toetsenbord?

Hoe bouw je een virtueel QML toetsenbord?

1. Introductie

Een virtueel toetsenbord is nodig wanneer er geen fysiek toetsenbord beschikbaar is. Meestal is dit het geval bij elektronische apparaten met een touchscreen.

Voor Qt-applicaties zijn er verschillende manieren om een virtueel toetsenbord toe te voegen. Doorgaans wordt één van de volgende opties gebruikt:

  • Het Qt Virtual Keyboard
  • Het toetsenbord dat door het platform zelf wordt voorzien, zoals het Apple-toetsenbord op een iPad
  • Een custom-built QML-toetsenbord

Invisto maakt vaak gebruik van de custom-built QML-methode, omdat we geen beperkingen willen opleggen op vlak van design of gebruiksgemak. Dit wordt bovendien ondersteund door het feit dat je met QML een grafisch ontwerp heel eenvoudig kunt reproduceren.

2. Toetsenbord

keyboard

In dit artikel focussen we op een toetsenbord dat ontworpen is voor het bewerken van patiënteninformatie. We bekijken hoe je:

  • Een basis lay-out voor een toetsenbord opzet
  • Het toetsenbord koppelt aan een tekstveld
  • Toetsen dynamisch verandert bij het schakelen tussen alfabetisch en hoofdletters

2.1 Lay-out van het toetsenbord

Voor deze toepassing houden we geen rekening met een responsive design: de schermdimensies zijn vast.

 
import QtQuick
import QtQuick.Controls

Item {
    id: keyboardRoot
    
    width: 1194
    height: 418
    
    property var focusedTextField: null
    
    enum Modes {
        HIDDEN,
        ALPHABETICAL,
        ALPHABETICAL_CAPS,
        NUMBERS_1,
        NUMBERS_2
    }
    
    QtObject {
        id: internals
        
        property bool singleShift: false
    }
    
    property int mode: Keyboard.ALPHABETICAL
    
    
    Item {
        id: keyboardHeader
        ...
    }
    
    Column {
        id: mainButtons
        
        Row {
            id: buttonsFirstRow 
            ...
        }
        
        Row {
            id: buttonsSecondRow 
            ...
        }
        
        Row {
            id: buttonsThirdRow 
            ...
        }
        
        Row {
            id: buttonsFourthRow 
            ...
        }
        
    }
    
    Rectangle {
        id: horizontalLine
        ...
    }
}

Zoals je kunt zien, gebruiken we een flexibele gridstructuur door een kolom te combineren met meerdere rijen. Elke rij bevat een reeks toetsenbordknoppen.

 
Row {
    id: buttonsFirstRow
    spacing: 12
    
    KeyboardButton {
        dark: true
        width: 105
        
        SingleLabel {
            text: "tab"
        }
    }
    
    ListModel {
        id: firstRowAlphabetical
        
        ListElement { first: "1"; second: "q" }
        ListElement { first: "2"; second: "w" }
        ListElement { first: "3"; second: "e" }
        ListElement { first: "4"; second: "r" }
        ListElement { first: "5"; second: "t" }
        ListElement { first: "6"; second: "y" }
        ListElement { first: "7"; second: "u" }
        ListElement { first: "8"; second: "i" }
        ListElement { first: "9"; second: "o" }
        ListElement { first: "0"; second: "p" }
    }
    
    ListModel {
        id: firstRowAlphabeticalCaps
        
        ListElement { first: "1"; second: "Q" }
        ListElement { first: "2"; second: "W" }
        ListElement { first: "3"; second: "E" }
        ListElement { first: "4"; second: "R" }
        ListElement { first: "5"; second: "T" }
        ListElement { first: "6"; second: "Y" }
        ListElement { first: "7"; second: "U" }
        ListElement { first: "8"; second: "I" }
        ListElement { first: "9"; second: "O" }
        ListElement { first: "0"; second: "P" }
    }
    
    Repeater {
        model: keyboardRoot.mode === Keyboard.ALPHABETICAL ? firstRowAlphabetical : firstRowAlphabeticalCaps
        
        KeyboardButton {
            DoubleCharacters {
                first: model.first
                second: model.second
            }
        }
    }
    
    KeyboardButton {
        dark: true
        width: 105
        
        SingleLabel {
            text: "delete"
            alignLeft: false
        }
    }
}

Laten we eens kijken hoe de knoppen zijn opgebouwd. In ons scenario is een knop gewoon een rechthoekige vorm met wat styling.

 
// KeyboardButton.qml
import QtQuick

Rectangle {
    id: root

    property bool dark: false

    color: dark ? "#45454B" : "#7C7C86"

    width: 82
    height: 74
    radius: 7

    border.color: "#222"

    MouseArea {
        id: mouseArea

        anchors.fill: root
        onPressed: {
            root.border.width = 2
        }

        onReleased: {
            root.border.width = 0
        }
    }
}

Om de gewenste inhoud boven op de knop weer te geven, kunnen we een child-element toevoegen, zoals:

  • SingleLabel
  • SingleIcon
  • DoubleLabel
 
KeyboardButton {
    dark: true
    width: 120
    
    SingleLabel {
        alignLeft: true
        text: ".?123"
    }
}

2.2 Het toetsenbord verbinden met een tekstveld

Om de koppeling tussen een toetsenbord en een bewerkbaar tekstveld te demonstreren, hebben we onze EditText.qml vereenvoudigd door overbodige elementen te verwijderen en enkel de relevante onderdelen te behouden. In essentie hebben we een QML TextInput met visuele verbeteringen, met daar bovenop een MouseArea.

 
// EditText.qml
import QtQuick
import QtQuick.Controls

Item {
    id: root
    
    property string title: ""
    property string text: ""
    property alias cursorPosition: textInputField.cursorPosition
    
    width: 550
    height: 88
    
    TextInput {
        id: textInputField
        
        text: root.text
        color: "white"
        font.family: theme.fontRegular
        font.pixelSize: 18
        anchors.verticalCenter: textBox.verticalCenter
        
        x: 27
    }
    
    
    MouseArea {
        anchors.fill: root
        onClicked: {
            textInputField.focus = true
            keyboard.focusedTextField = root
        }
    }
}

Wat gebeurt er wanneer je op een tekstveld klikt?

  • We geven de focus aan onze TextInput. Hierdoor wordt de cursor zichtbaar.
  • We informeren het toetsenbord welk tekstveld de focus heeft gekregen: keyboard.focusedTextField = root

Wat gebeurt er aan de kant van het toetsenbord wanneer een knop wordt ingedrukt?

 
// DoubleLabel.qml
import QtQuick

Item {
    id: root

    anchors.fill: parent

    property string first: ""
    property string second: ""

    Text {
        text: root.first
        anchors.horizontalCenter: root.horizontalCenter
        y: 9

        font.pixelSize: 17
        font.family: theme.fontRegular
        color: Qt.rgba(235, 235, 245, 0.3)
    }

    Text {
        text: root.second
        anchors.horizontalCenter: root.horizontalCenter
        y: 31

        font.pixelSize: 28
        font.family: theme.fontRegular
        color: "#fff"
    }

    MouseArea {
        id: mouseArea
        anchors.fill: root
        propagateComposedEvents: true
        onPressed: {
            keyboardRoot.handleButtonPressed(root.second)
            mouse.accepted = false
        }
    }
}

  • De inhoudselementen (SingleIcon, SingleLabel, DoubleLabel) roepen elk de functie keyboard.handleButtonPressed(label) aan.
  • In deze handler worden acties afgehandeld zoals een karakter toevoegen of verwijderen.

 
function handleButtonPressed(button)
{
    if (button === "delete")
    {
        var str = keyboardRoot.focusedTextField.text
        if (str.length)
        {
            
            str = str.substring(0, str.length - 1);
            keyboardRoot.focusedTextField.text = str
            keyboardRoot.focusedTextField.cursorPosition = str.length
        }
    }
    else if (button === "caps lock")
    {
        if (keyboardRoot.mode === Keyboard.ALPHABETICAL)
        {
            keyboardRoot.mode = Keyboard.ALPHABETICAL_CAPS
        }
        else if (keyboardRoot.mode === Keyboard.ALPHABETICAL_CAPS)
        {
            keyboardRoot.mode = Keyboard.ALPHABETICAL
        }
    }
    else if (button === "tab")
    {
        keyboardRoot.focusedTextField.text = keyboardRoot.focusedTextField.text + "\t"
        keyboardRoot.focusedTextField.cursorPosition = keyboardRoot.focusedTextField.text.length
    }
    else if (button === "shift")
    {
        if (keyboardRoot.mode === Keyboard.ALPHABETICAL)
        {
            keyboardRoot.mode = Keyboard.ALPHABETICAL_CAPS
            internals.singleShift = true
        }
    }
    else if (button === ".?123")
    {
        keyboardRoot.mode = Keyboard.NUMBERS_1
    }
    else if (button === "return")
    {
        keyboardRoot.mode = Keyboard.HIDDEN
    }
    else if (button === "")
    {
        keyboardRoot.focusedTextField.text = keyboardRoot.focusedTextField.text + " "
        keyboardRoot.focusedTextField.cursorPosition = keyboardRoot.focusedTextField.text.length
    }
    else
    {
        keyboardRoot.focusedTextField.text = keyboardRoot.focusedTextField.text + button
        keyboardRoot.focusedTextField.cursorPosition = keyboardRoot.focusedTextField.text.length
        
        if (internals.singleShift)
        {
            internals.singleShift = false
            keyboardRoot.mode = Keyboard.ALPHABETICAL
        }
    }
}

2.3 Wisselen tussen alfabetisch en hoofdletters

Voor elke rijhebben we twee modellen gedefinieerd:

  • firstRowAlphabetical
  • firstRowAlphabeticalCaps

Afhankelijk van de modus van het toetsenbord geven we één van deze modellen door aan de model-property van de Repeater.

 
Row {
    id: buttonsFirstRow
    spacing: 12
    
    KeyboardButton {
        dark: true
        width: 105
        
        SingleLabel {
            text: "tab"
        }
    }
    
    ListModel {
        id: firstRowAlphabetical
        
        ListElement { first: "1"; second: "q" }
        ListElement { first: "2"; second: "w" }
        ListElement { first: "3"; second: "e" }
        ListElement { first: "4"; second: "r" }
        ListElement { first: "5"; second: "t" }
        ListElement { first: "6"; second: "y" }
        ListElement { first: "7"; second: "u" }
        ListElement { first: "8"; second: "i" }
        ListElement { first: "9"; second: "o" }
        ListElement { first: "0"; second: "p" }
    }
    
    ListModel {
        id: firstRowAlphabeticalCaps
        
        ListElement { first: "1"; second: "Q" }
        ListElement { first: "2"; second: "W" }
        ListElement { first: "3"; second: "E" }
        ListElement { first: "4"; second: "R" }
        ListElement { first: "5"; second: "T" }
        ListElement { first: "6"; second: "Y" }
        ListElement { first: "7"; second: "U" }
        ListElement { first: "8"; second: "I" }
        ListElement { first: "9"; second: "O" }
        ListElement { first: "0"; second: "P" }
    }
    
    Repeater {
        model: keyboardRoot.mode === Keyboard.ALPHABETICAL ? firstRowAlphabetical : firstRowAlphabeticalCaps
        
        KeyboardButton {
            DoubleCharacters {
                first: model.first
                second: model.second
            }
        }
    }
    
    KeyboardButton {
        dark: true
        width: 105
        
        SingleLabel {
            text: "delete"
            alignLeft: false
        }
    }
}

3. Qt WebBrowser als vereiste? No Pasaran!

De custom-built QML-implementatie die in dit artikel wordt beschreven, voldoet niet aan de vereisten wanneer je HTML <input>-velden gebruikt in combinatie met Qt WebBrowser. In dat geval wordt de aanpak complexer en moet je mogelijk kijken naar QInputMethod, QPlatformInputContext, QPA, en andere componenten. Hoewel dit buiten de scope van deze case valt, is het wel de moeite waard om te vermelden.

4. Conclusie

Een mooi en functioneel toetsenbord dat volledig op jouw noden is afgestemd, vereist een custom-built QML-toetsenbord. Dit artikel wil je een introductie geven over hoe je hiermee kunt starten.

Heb je meer info nodig over dit onderwerp? Neem gerust contact met ons op. We delen hier niet de volledige broncode, omdat die deel uitmaakt van een project. Maar we kunnen je altijd een werkend voorbeeld bezorgen indien gewenst.

Download de volledige case

Ook interesse in een state-of-the-art interface die uw product naar een hoger niveau tilt?

Let's talk!

Enkele van onze klanten

Logo HoneywellLogo Picanol GroupLogo CiscoLogo BekaertLogo Universiteit GentLogo Renson