Creating QML Controls From Scratch: Keyboard
In the final installment in our QML Controls from Scratch series, this time we will implement an English-only Keyboard. There are typically three ways to display a virtual keyboard in a QML app:
1. Qt Virtual Keyboard
2. Use the keyboard that ships with the operating system (e.g. on Windows 10 call TabTip.exe in Tablet mode)
3. Roll your own virtual keyboard in QML
If the keyboard must match a designer mockup, 3 is usually the only option, which is the approach we'll take here.
Our Keyboard implementation consists of three QML files:
Keyboard.qml: renders the full-screen keyboard, and reuses our Button control (not used directly by clients)
KeyboardController.qml: non-visual component to show Keyboard.qml and retrieve the text string the user typed in
KeyboardInput.qml: TextInput that brings up Keyboard via KeyboardController
KeyboardController can be used to bring up the Keyboard from anywhere by calling its show() method. Internally, it creates and destroys the Keyboard as necessary. While memory-efficient, the Keyboard can be slow to come up on older systems. If there is enough memory, you can make the Keyboard a singleton instead to always keep it in memory (trading memory usage for speed). KeyboardConroller uses a trick to reparent the Keyboard to the application's rootObject() such that the Keyboard is always on top of everything else.
Keyboard.qml
import QtQuick 2.0
Item {
id: root
// public
property bool password: false
signal accepted(string text); // onAccepted: print('onAccepted', text)
signal rejected(); // onRejected: print('onRejected')
// private
width: 500; height: 500 // default size
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
property bool shift: false
property bool symbols: false
property double columns: 10
property double rows: 5
MouseArea {anchors.fill: parent} // don't allow touches to pass to MouseAreas underneath
Rectangle { // input
width: root.width; height: 0.2 * root.height
Button { // close v
id: closeButton
text: '\u2193' // BLACK DOWN-POINTING TRIANGLE
width: height; height: 0.8 * parent.height
anchors.verticalCenter: parent.verticalCenter
x: columnSpacing
onClicked: rejected() // emit
}
TextInput {
id: textInput
cursorVisible: true
anchors {left: closeButton.right; right: clearButton.left; verticalCenter: parent.verticalCenter; margins: 0.03 * root.width}
font.pixelSize: 0.5 * parent.height
clip: true
echoMode: password? TextInput.Password: TextInput.Normal
onAccepted: if(acceptableInput) root.accepted(text) // keyboard Enter key
}
Button { // clear x
id: clearButton
text: '\u2715' // BLACK DOWN-POINTING TRIANGLE
width: height; height: 0.8 * parent.height
anchors {verticalCenter: parent.verticalCenter; right: parent.right; rightMargin: columnSpacing}
enabled: textInput.text
onClicked: textInput.text = ''
}
}
Rectangle {
width: parent.width; height: 0.8 * parent.height
anchors.bottom: parent.bottom
Item { // keys
id: keyboard
anchors {fill: parent; leftMargin: columnSpacing}
Column {
spacing: columnSpacing
Row { // 1234567890
spacing: rowSpacing
Repeater {
model: [
{text: '1', width: 1},
{text: '2', width: 1},
{text: '3', width: 1},
{text: '4', width: 1},
{text: '5', width: 1},
{text: '6', width: 1},
{text: '7', width: 1},
{text: '8', width: 1},
{text: '9', width: 1},
{text: '0', width: 1},
]
delegate: Button {
text: modelData.text
width: modelData.width * keyboard.width / columns - rowSpacing
height: keyboard.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row { // qwertyuiop
spacing: rowSpacing
Repeater {
model: [
{text: 'q', symbol: '+', width: 1},
{text: 'w', symbol: '\u00D7', width: 1}, // MULTIPLICATION SIGN
{text: 'e', symbol: '\u00F7', width: 1}, // DIVISION SIGN
{text: 'r', symbol: '=', width: 1},
{text: 't', symbol: '/', width: 1},
{text: 'y', symbol: '_', width: 1},
{text: 'u', symbol: '<', width: 1},
{text: 'i', symbol: '>', width: 1},
{text: 'o', symbol: '[', width: 1},
{text: 'p', symbol: ']', width: 1},
]
delegate: Button {
text: symbols? modelData.symbol: shift? modelData.text.toUpperCase(): modelData.text
width: modelData.width * keyboard.width / columns - rowSpacing
height: keyboard.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row { // asdfghjkl
spacing: rowSpacing
Repeater {
model: [
{text: '', symbol: '', width: 0.5},
{text: 'a', symbol: '!', width: 1},
{text: 's', symbol: '@', width: 1},
{text: 'd', symbol: '#', width: 1},
{text: 'f', symbol: '$', width: 1},
{text: 'g', symbol: '%', width: 1},
{text: 'h', symbol: '&', width: 1},
{text: 'j', symbol: '*', width: 1},
{text: 'k', symbol: '(', width: 1},
{text: 'l', symbol: ')', width: 1},
{text: '', symbol: '', width: 0.5},
]
delegate: Button {
text: symbols? modelData.symbol: shift? modelData.text.toUpperCase(): modelData.text
width: modelData.width * keyboard.width / columns - rowSpacing
height: keyboard.height / rows - columnSpacing
onClicked: root.clicked(text)
}
}
}
Row { // zxcvbnm
spacing: rowSpacing
Repeater {
model: [
{text: '\u2191', symbol: '', width: 1.5}, // UPWARDS ARROW (shift)
{text: 'z', symbol: '-', width: 1},
{text: 'x', symbol: "'", width: 1},
{text: 'c', symbol: '"', width: 1},
{text: 'v', symbol: ':', width: 1},
{text: 'b', symbol: ';', width: 1},
{text: 'n', symbol: ',', width: 1},
{text: 'm', symbol: '?', width: 1},
{text: '\u2190', symbol: '\u2190', width: 1.5}, // LEFTWARDS ARROW (backspace)
]
delegate: Button {
text: symbols? modelData.symbol: shift? modelData.text.toUpperCase(): modelData.text
width: modelData.width * keyboard.width / columns - rowSpacing
height: keyboard.height / rows - columnSpacing
enabled: text == '\u2190'? textInput.text: true // LEFTWARDS ARROW (backspace)
onClicked: root.clicked(text)
}
}
}
Row { // space
spacing: rowSpacing
Repeater {
model: [
{text: symbols? 'AB': '@#', width: 1.5},
{text: ',', width: 1},
{text: ' ', width: 5}, // space
{text: '.', width: 1},
{text: '\u21B5', width: 1.5}, // DOWNWARDS ARROW WITH CORNER LEFTWARDS (enter)
]
delegate: Button {
text: modelData.text
width: modelData.width * keyboard.width / columns - rowSpacing
height: keyboard.height / rows - columnSpacing
enabled: text == '\u21B5'? textInput.text: true // DOWNWARDS ARROW WITH CORNER LEFTWARDS (enter)
onClicked: root.clicked(text)
}
}
}
}
}
}
signal clicked(string text)
onClicked: {
if( text == '\u2190') { // LEFTWARDS ARROW (backspace)
var position = textInput.cursorPosition
textInput.text = textInput.text.substring(0, textInput.cursorPosition - 1) +
textInput.text.substring(textInput.cursorPosition, textInput.text.length)
textInput.cursorPosition = position - 1
}
else if(text == '\u2191') shift = !shift // UPWARDS ARROW (shift)
else if(text == '@#' ) symbols = true
else if(text == 'AB' ) symbols = false
else if(text == '\u21B5') accepted(textInput.text) // DOWNWARDS ARROW WITH CORNER LEFTWARDS (enter)
else { // insert text
var position = textInput.cursorPosition
textInput.text = textInput.text.substring(0, textInput.cursorPosition) + text +
textInput.text.substring(textInput.cursorPosition, textInput.text.length)
textInput.cursorPosition = position + 1
shift = false // momentary
}
}
}
KeyboardController.qml
import QtQuick 2.0
Item {
id: root
// public
property bool password: false
signal accepted(string text); // onAccepted: print('onAccepted', text)
function show() {
keyboard = keyboardComponent.createObject(null, {password: root.password})
var rootObject = null, object = parent // search up the parent chain to find QQuickView::rootObject()
while(object) {
if(object) rootObject = object
object = object.parent
}
keyboard.parent = rootObject
keyboard.width = rootObject.width // resize
keyboard.height = rootObject.height
}
// private
property Item keyboard: null
Component {id: keyboardComponent; Keyboard {}}
Connections {
target: keyboard
onAccepted: {
root.accepted(text) // emit
keyboard.destroy() // hide
}
onRejected: keyboard.destroy() // hide
}
}
KeyboardInput.qml
import QtQuick 2.0
Rectangle { // TextInput from virtual keyboard
id: root
// public
property string label: 'label'
property bool password: false
property alias text: textInput.text // in/out
signal accepted(string text); // onAccepted: print('onAccepted', text)
// private
width: 500; height: 100 // default size
border.width: 0.05 * root.height
radius: 0.2 * height
opacity: enabled && !mouseArea.pressed? 1: 0.3 // disabled/pressed state
Text { // label
visible: !textInput.text
text: label
anchors {left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: parent.radius}
font.pixelSize: 0.5 * parent.height
opacity: 0.3
}
TextInput {
id: textInput
anchors {left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter; margins: parent.radius}
font.pixelSize: 0.5 * parent.height
echoMode: password? TextInput.Password: TextInput.Normal
}
MouseArea { // comment out to input text via physical keyboard
id: mouseArea
anchors.fill: parent
onClicked: keyboardController.show()
}
KeyboardController {
id: keyboardController
password: root.password
onAccepted: {
textInput.text = text
root.accepted(text) // emit
}
}
}
Test.qml
import QtQuick 2.0
KeyboardInput {
label: 'Username'
onAccepted: print('onAccepted', text)
}
Summary
In this installment, we created a Keyboard. The source code can be downloaded here.