0
votes

I am using PyQt5 together with QML to create an application. I need to be able to simulate keyboard board events either from PyQT to my QML objects or alternatively from QML itself.

I'm using a QQmlApplicationEngine to load my QML and I am using a "back end" QObject in Python to connect to signals and slots in QML.

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
backend = Backend()
engine.rootContext().setContextProperty("backend", backend)
engine.load('./qml/main.qml')
app.setEngine(engine)

Later on I try to send a key event:

app.sendEvent(engine.rootObjects()[0].focusObject(), QKeyEvent(QEvent.KeyRelease, Qt.Key_Down, Qt.NoModifier))

In my QML I have a list view which has the focus. If I press the up and down keys on my keyboard the focused item in the list changes as expected. However, when I try sending a key event using the code above, the list view does not react.

The engine.rootObjects()[0] is a QObject when printed.

QML snippet:

ApplicationWindow {
    // list model here
    // list delegate here
    ListView {
        id: menuView
        anchors.fill: parent
        focus: true
        model: menuModel
        delegate: menuDelegate
        keyNavigationEnabled: true
        keyNavigationWraps: true
   }
}

Alternatively, I wondered if it is possible to generate a key event from within QML itself by interacting with activeFocusItem of the ApplicationWindow object? I haven't been able to get this to work either.

2

2 Answers

0
votes

This did the trick in the end after looking at how the QtGamepad class generates key events:

QGuiApplication.sendEvent(app.focusWindow(), QKeyEvent(QEvent.KeyPress, Qt.Key_Down, Qt.NoModifier))

Now my QML application responds in the same way as if the user had pressed a key.

0
votes

You have 2 errors:

  • The focusObject() method returns the QObject that has the focus, in your particular case it is one of the items of the ListView delegate that although receiving the mouse event will not change the selected item since only that ListView does.

  • If you want to send the mouse event, you must first send the KeyPress, otherwise the keyRelease method will never be triggered, although many times only the first event is necessary.

Considering the above, the solution is to send the event to the ListView directly or to another object that forwards it to the ListView, such as the window, in addition to sending the QEvent :: KeyPress. In the following example using timers, the event will be sent to the window or object from python and QML, respectively:

from functools import partial
from PyQt5 import QtCore, QtGui, QtQml


class KeyboardBackend(QtCore.QObject):
    @QtCore.pyqtSlot(QtCore.QObject)
    def moveUp(self, obj):
        print("up")
        event = QtGui.QKeyEvent(
            QtCore.QEvent.KeyPress, QtCore.Qt.Key_Up, QtCore.Qt.NoModifier
        )
        QtCore.QCoreApplication.sendEvent(obj, event)

    @QtCore.pyqtSlot(QtCore.QObject)
    def moveDown(self, obj):
        print("down")
        event = QtGui.QKeyEvent(
            QtCore.QEvent.KeyPress, QtCore.Qt.Key_Down, QtCore.Qt.NoModifier
        )
        QtCore.QCoreApplication.sendEvent(obj, event)


if __name__ == "__main__":
    import os
    import sys

    app = QtGui.QGuiApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()

    keyboard_backed = KeyboardBackend()
    engine.rootContext().setContextProperty("keyboard_backed", keyboard_backed)
    file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "main.qml")
    engine.load(QtCore.QUrl.fromLocalFile(file))

    if not engine.rootObjects():
        sys.exit(-1)
    root = engine.rootObjects()[0]
    timer = QtCore.QTimer(timeout=partial(keyboard_backed.moveUp, root), interval=1000)
    QtCore.QTimer.singleShot(500, timer.start)
    sys.exit(app.exec())
import QtQuick 2.12
import QtQuick.Controls 2.12

ApplicationWindow {
    id: root
    visible: true
    width: 640
    height: 480
    ListModel {
        id: menuModel
        Component.onCompleted:{
            ['A', 'B', 'C', 'D'].forEach(function(letter) {
                menuModel.append({"name": letter})
            });
        }
    }
    Component{
        id: menuDelegate
        Text {
            text: name
        }
    }
    Timer{
        interval: 1000; running: true; repeat: true
        onTriggered: keyboard_backed.moveDown(lv)
    }
    ListView {
        id: lv
        anchors.top: parent.top
        focus: true
        model: menuModel
        delegate: menuDelegate
        keyNavigationEnabled: true
        keyNavigationWraps: true
        highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
        height: 100
    }
}