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.
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.
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
}
}
Related
I would like to test my QML frontend code along with my Python backend code(using PySide2) with Pytest preferably, and be able to send keyClicks, MouseClicks and signals just like pytest-qt plugin does. I have already checked out pytest-qml, but the test code is written via QML, and then only ran via via pytest, but I would like to send events and such from python itself, not QML
Basically, having the python code as such:
"""
Slots, Signals, context class etc etc...
"""
app = QGuiApplication([])
engine = QQmlApplicationEngine()
engine.load(QUrl.fromLocalFile("main.qml"))
app.exec_()
and a simple main.qml file, as such,
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.15
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
title: "mydialog"
onAccepted: {
}
}
Button {
objectName: "mybtn"
width: 200
height: 200
id: btn
text: "hello"
onClicked: {
openDialog.open()
}
}
}
I would like to do (pseudo-code)something like
def test_file_open():
#Grab QQuickItem(btn)
#Send mouse event to click btn
#Send string to file dialog
# assert string sent == string selected
The pytest-qt plugin would work, but functions take QWidget and QML deals with QQuickItems, which as far as I know doesnt deal with QWidgets.
Is it even possible, or my only option to test my app slots etc is via the pytest-qml ? Perhaps its the easiest way, but perhaps there are other options :)
Edit:
If you use import Qt.labs.platform 1.1 instead of the import QtQuick.Dialogs 1.3, and force QML to not use native dialog, then just change
# assert myfiledialog.property("fileUrl").toLocalFile() == filename # uses QDialog
assert myfiledialog.property("currentFile").toLocalFile() == filename # using QLabs Dialog
And then using the rest of the code from accepted answer it will work, so apparently its very important that it does not use a native dialog.
If anyone else in the future knows how to make it work with native dialog and using QtQuick.Dialogs 1.3 as the original question presented, it would be nice :). But this is still nice to test overall!
You can use the same API since pytest-qt is based on QtTest. Obviously you must understand the structure of the application, for example that the FileDialog is just a QObject that only manages a QWindow that has the dialog, in addition to managing the positions of the items with respect to the windows.
import os
from pathlib import Path
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine
CURRENT_DIR = Path(__file__).resolve().parent
def build_engine():
engine = QQmlApplicationEngine()
filename = os.fspath(CURRENT_DIR / "main.qml")
url = QUrl.fromLocalFile(filename)
engine.load(url)
return engine
def main():
app = QGuiApplication([])
engine = build_engine()
app.exec_()
if __name__ == "__main__":
main()
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
ApplicationWindow {
id: mywin
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
visible: true
FileDialog {
id: openDialog
objectName: "myfiledialog"
title: "mydialog"
onAccepted: {
}
}
Button {
id: btn
objectName: "mybtn"
width: 200
height: 200
text: "hello"
onClicked: {
openDialog.open();
}
}
}
import os
from PySide2.QtCore import QCoreApplication, QObject, Qt, QPointF
from PySide2.QtGui import QGuiApplication
from PySide2.QtQuick import QQuickItem
from PySide2.QtWidgets import QApplication
import pytest
from app import build_engine
#pytest.fixture(scope="session")
def qapp():
QCoreApplication.setOrganizationName("qapp")
QCoreApplication.setOrganizationDomain("qapp.com")
QCoreApplication.setAttribute(Qt.AA_DontUseNativeDialogs)
yield QApplication([])
def test_app(tmp_path, qtbot):
engine = build_engine()
assert QCoreApplication.testAttribute(Qt.AA_DontUseNativeDialogs)
with qtbot.wait_signal(engine.objectCreated, raising=False):
assert len(engine.rootObjects()) == 1
root_object = engine.rootObjects()[0]
root_item = root_object.contentItem()
mybtn = root_object.findChild(QQuickItem, "mybtn")
assert mybtn is not None
center = QPointF(mybtn.width(), mybtn.height()) / 2
qtbot.mouseClick(
mybtn.window(),
Qt.LeftButton,
pos=root_item.mapFromItem(mybtn, center).toPoint(),
)
qtbot.wait(1000)
qfiledialog = None
for window in QGuiApplication.topLevelWindows():
if window is not root_object:
qfiledialog = window
assert qfiledialog is not None, QGuiApplication.topLevelWindows()
file = tmp_path / "foo.txt"
file.touch()
filename = os.fspath(file)
for letter in filename:
qtbot.keyClick(qfiledialog, letter, delay=100)
qtbot.wait(1000)
qtbot.keyClick(qfiledialog, Qt.Key_Return)
qtbot.wait(1000)
myfiledialog = root_object.findChild(QObject, "myfiledialog")
assert myfiledialog is not None
assert myfiledialog.property("fileUrl").toLocalFile() == filename
Note: The test may fail if the filedialog uses the native window, you could use tools like pyinput but a simpler option is to use virtualenv.
I'm trying to change Image with Pushbutton (GPIO PINs) stored inside a folder using QML with PyQt5
Python Code:
from gpiozero import Button
from signal import pause
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtQml import *
import os, time, sys
def btn_pressed():
global r
return lambda: r.setProperty("source", "/home/pi/Desktop/example/sample/img/img1.jpg")
button1 = Button(20)
myApp = QGuiApplication([])
myEngine = QQmlApplicationEngine()
directory = os.path.dirname(os.path.abspath(__file__))
myEngine.load(QUrl.fromLocalFile(os.path.join(directory, 'simple1.qml')))
if not myEngine.rootObjects():
print("root object not found")
r = myEngine.rootObjects()[0].findChild(QObject, "DisplayImage")
dir(r)
print("Main Thead id ",myApp.thread())
updateUI = UpdateUI()
button1.when_pressed = btn_pressed()
myEngine.quit.connect(myApp.quit)
sys.exit(myApp.exec_())
QML:
import QtQuick 2.10
import QtQuick.Controls 1.6
import QtQuick.Window 2.2
ApplicationWindow {
id : main
title: qsTr("Test")
width: 640
height: 480
visible: true
Rectangle{
width: parent.width
height: parent.height
Image {
id: img
source: ""
width : main.width;
fillMode : Image.PreserveAspectFit
objectName: "DisplayImage"
}
}
}
When I Pressed the Push Button Connected to GPIO 20 in raspberry Pi 4 , I'm Getting below error message.
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QQmlApplicationEngine(0x10f5ba0), parent's thread is QThread(0xf6f7c0), current thread is QThread(0xa98007c8)
Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode()
I also tried creating class with a method changing Image Source Property and then calling the same method(via Class Object) upon PushButton press but it shows same error message
Is there a way to set Image's Source Property in QML - from parent thread in Python.
In Winforms we can Avoid "Cross-Thread Violation Error" by using Delegates.
Can we Use Signal and Slot to Solve this Problem in PyQt5.
gpiozero uses threads to be able to monitor the gpio so as not to block the main GUI, so the associated function when_pressed will be executed in that thread but Qt prohibits updating GUI elements such as the image from another thread.
The solution is to create a QObject that emits the signal in the method associated with when_pressed since the signals are thread-safe.
On the other hand it is not good to modify the QML elements from C++/Python, it is better to export the QObject to QML and make the connections in that scope.
import os
import sys
from gpiozero import Button
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
class ButtonManager(QObject):
pressed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._button = Button(20)
self._button.when_pressed = self._on_when_pressed
def _on_when_pressed(self):
self.pressed.emit()
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
button_manager = ButtonManager()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("button_manager", button_manager)
current_dir = os.path.dirname(os.path.abspath(__file__))
engine.load(QUrl.fromLocalFile(os.path.join(current_dir, "simple1.qml")))
if not engine.rootObjects():
print("root object not found")
sys.exit(-1)
engine.quit.connect(app.quit)
sys.exit(app.exec_())
simple1.qml
import QtQuick 2.10
import QtQuick.Controls 1.6
import QtQuick.Window 2.2
ApplicationWindow {
id : main
title: qsTr("Test")
width: 640
height: 480
visible: true
Rectangle{
anchors.fill: parent
Image {
id: img
width : main.width
fillMode : Image.PreserveAspectFit
}
}
Connections{
target: button_manager
onPressed: img.source = "/home/pi/Desktop/example/sample/img/img1.jpg"
}
}
I want to write a simple desktop application on Ubuntu and I thought that an easy way was to use Qt with QML as GUI and Python as the language for the logic, since I am somewhat familiar with Python.
Now I am trying for hours to somehow connect the GUI and the logic, but it is not working.
I managed the connection QML --> Python but not the other way around. I have Python classes which represent my data model and I added JSON encode and decode functions. So for now there is no SQL database involved. But maybe a direct connection between QML view and some database would make things easier?
So now some code.
QML --> Python
The QML file:
ApplicationWindow {
// main window
id: mainWindow
title: qsTr("Test")
width: 640
height: 480
signal tmsPrint(string text)
Page {
id: mainView
ColumnLayout {
id: mainLayout
Button {
text: qsTr("Say Hello!")
onClicked: tmsPrint("Hello!")
}
}
}
}
Then I have my slots.py:
from PySide2.QtCore import Slot
def connect_slots(win):
win.tmsPrint.connect(say_hello)
#Slot(str)
def say_hello(text):
print(text)
And finally my main.py:
import sys
from controller.slots import connect_slots
from PySide2.QtWidgets import QApplication
from PySide2.QtQml import QQmlApplicationEngine
if __name__ == '__main__':
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load('view/main.qml')
win = engine.rootObjects()[0]
connect_slots(win)
# show the window
win.show()
sys.exit(app.exec_())
This works fine and I can print "Hello!". But is this the best way to do it or is it better to create a class with slots and use setContextProperty to be able to call them directly without adding additional signals?
Python --> QML
I cannot get this done. I tried different approaches, but none worked and I also don't know which one is the best to use. What I want to do is for example show a list of objects and offer means to manipulate data in the application etc.
include Javascript:
I added an additional file application.js with a function just to print something, but it could probably be used to set the context of a text field etc.
Then I tried to use QMetaObject and invokeMethod, but just got errors with wrong arguments etc.
Does this approach make any sense? Actually I don't know any javascript, so if it is not necessary, I would rather not use it.
ViewModel approach
I created a file viewmodel.py
from PySide2.QtCore import QStringListModel
class ListModel(QStringListModel):
def __init__(self):
self.textlines = ['hi', 'ho']
super().__init__()
And in the main.py I added:
model = ListModel()
engine.rootContext().setContextProperty('myModel', model)
and the ListView looks like this:
ListView {
width: 180; height: 200
model: myModel
delegate: Text {
text: model.textlines
}
}
I get an error "myModel is not defined", but I guess that it can't work anyway, since delegates only take one element and not a list.
Is this approach a good one? and if yes, how do I make it work?
Is there a totally different approach to manipulate data in a QML view?
I appreciate your help!
I know the Qt documentation but I am not happy with it. So maybe I am missing something. But PyQt seems to be way more popular than PySide2 (at least google searches seem to indicate that) and PySide references often use PySide1 or not the QML QtQuick way of doing things...
Your question has many aspects so I will try to be detailed in my answer and also this answer will be continuously updated because this type of questions are often asked but they are solutions for a specific case so I am going to take the liberty of giving it a general approach and be specific in the possible scenarios.
QML to Python:
Your method works because the type conversion in python is dynamic, in C++ it does not happen. It works for small tasks but it is not maintainable, the logic must be separated from the view so it should not be dependent. To be concrete, let's say that the printed text will be taken by the logic to perform some processing, then if you modify the name of the signal, or if the data does not depend on ApplicationWindow but on another element, etc. then you will have to change a lot connection code.
The recommended as you indicate is to create a class that is responsible for mapping the data you need your logic and embed it in QML, so if you change something in the view you just change the connection:
Example:
main.py
import sys
from PySide2.QtCore import QObject, Signal, Property, QUrl
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
class Backend(QObject):
textChanged = Signal(str)
def __init__(self, parent=None):
QObject.__init__(self, parent)
self.m_text = ""
#Property(str, notify=textChanged)
def text(self):
return self.m_text
#text.setter
def setText(self, text):
if self.m_text == text:
return
self.m_text = text
self.textChanged.emit(self.m_text)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
backend = Backend()
backend.textChanged.connect(lambda text: print(text))
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("backend", backend)
engine.load(QUrl.fromLocalFile('main.qml'))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
main.qml
import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
ApplicationWindow {
title: qsTr("Test")
width: 640
height: 480
visible: true
Column{
TextField{
id: tf
text: "Hello"
}
Button {
text: qsTr("Click Me")
onClicked: backend.text = tf.text
}
}
}
Now if you want the text to be provided by another element you just have to change the line: onClicked: backend.text = tf.text.
Python to QML:
I can not tell you what you did wrong with this method because you do not show any code, but I do indicate the disadvantages. The main disadvantage is that to use this method you must have access to the method and for that there are 2 possibilities, the first one is that it is a rootObjects as it is shown in your first example or searching through the objectName, but it happens that you initially look for the object, you get it and this is removed from QML, for example the Pages of a StackView are created and deleted every time you change pages so this method would not be correct.
The second method for me is the correct one but you have not used it correctly, unlike the QtWidgets that focus on the row and the column in QML the roles are used. First let's implement your code correctly.
First textlines is not accessible from QML since it is not a qproperty. As I said you must access through the roles, to see the roles of a model you can print the result of roleNames():
model = QStringListModel()
model.setStringList(["hi", "ho"])
print(model.roleNames())
output:
{
0: PySide2.QtCore.QByteArray('display'),
1: PySide2.QtCore.QByteArray('decoration'),
2: PySide2.QtCore.QByteArray('edit'),
3: PySide2.QtCore.QByteArray('toolTip'),
4: PySide2.QtCore.QByteArray('statusTip'),
5: PySide2.QtCore.QByteArray('whatsThis')
}
In the case that you want to obtain the text you must use the role Qt::DisplayRole, whose numerical value according to the docs is:
Qt::DisplayRole 0 The key data to be rendered in the form of text. (QString)
so in QML you should use model.display(or only display). so the correct code is as follows:
main.py
import sys
from PySide2.QtCore import QUrl, QStringListModel
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
model = QStringListModel()
model.setStringList(["hi", "ho"])
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("myModel", model)
engine.load(QUrl.fromLocalFile('main.qml'))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
main.qml
import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
ApplicationWindow {
title: qsTr("Test")
width: 640
height: 480
visible: true
ListView{
model: myModel
anchors.fill: parent
delegate: Text { text: model.display }
}
}
If you want it to be editable you must use the model.display = foo:
import QtQuick 2.10
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
ApplicationWindow {
title: qsTr("Test")
width: 640
height: 480
visible: true
ListView{
model: myModel
anchors.fill: parent
delegate:
Column{
Text{
text: model.display
}
TextField{
onTextChanged: {
model.display = text
}
}
}
}
}
There are many other methods to interact with Python/C++ with QML but the best methods involve embedding the objects created in Python/C++ through setContextProperty.
As you indicate the docs of PySide2 is not much, it is being implemented and you can see it through the following link. What exists most are many examples of PyQt5 so I recommend you understand what are the equivalences between both and make a translation, this translation is not hard since they are minimal changes.
I've noticed that QML can receive a signal emitted from Python by using the Connections object. Unfortunately, I can't figure out how to get that object to receive the arguments of that signal.
I've created a minimal test case that demonstrates what I want to do:
min.py
from PySide import QtCore, QtGui, QtDeclarative
import sys
# init Qt
app = QtGui.QApplication(sys.argv)
# set up the signal
class Signaller(QtCore.QObject):
emitted = QtCore.Signal(str)
signaller = Signaller()
# Load the QML
qt_view = QtDeclarative.QDeclarativeView()
context = qt_view.rootContext()
context.setContextProperty('signaller', signaller)
qt_view.setResizeMode(QtDeclarative.QDeclarativeView.SizeRootObjectToView)
qt_view.setSource('min.qml')
qt_view.show()
# launch the signal
signaller.emitted.emit("Please display THIS text!")
# Run!
app.exec_()
And min.qml
import QtQuick 1.0
Rectangle {
width:300; height:100
Text {
id: display
text: "No signal yet detected!"
Connections {
target: signaller
onEmitted: {
display.text = "???" //how to get the argument?
}
}
}
}
As of Qt for Python versions 5.12.5, 5.13.1 it is working the same as in PyQt:
from PySide2.QtCore import Signal
sumResult = Signal(int, arguments=['sum'])
sumResult.emit(42)
QML:
onSumResult: console.log(sum)
As of Qt 4.8, PySide doesn't handle signal parameter names at all.
But you can create a QML signal with named parameters and connect your python signal to it using Javascript:
import QtQuick 1.0
Rectangle {
width:300; height:100
Text {
id: display
text: "No signal yet detected!"
signal reemitted(string text)
Component.onCompleted: signaller.emitted.connect(reemitted)
onReemitted: {
display.text = text;
}
}
}
Sorry i can't comment since i need to have higher reputation.
In response to the "Cannot assign to non-existent property", it's caused by the order in which you initialize your application.
Your root context object needs to be created before the engine.
Ok:
context = Context()
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("context", context)
Not ok:
engine = QQmlApplicationEngine()
context = Context()
engine.rootContext().setContextProperty("context", context)
How can bind the buttons I create in a .qml script to python PyQt5 code?
example:
python:
import sys
from PyQt5.QtCore import QObject, QUrl, Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import QQmlApplicationEngine
if __name__ == "__main__":
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
ctx = engine.rootContext()
ctx.setContextProperty("main", engine)
engine.load('test.qml')
win = engine.rootObjects()[0]
win.show()
sys.exit(app.exec_())
qml:
import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.2
import QtQuick.Dialogs 1.1
ApplicationWindow {
title: qsTr("Test Invoke")
width: 200
height: 100
Button{
y : 70
text : "About"
onClicked: {
print('Hello')
}
}
}
How can I do something with Python when the button is clicked?
Also: Does anyone has a good resource of examples or doc. about pyqt + qml (qt quick)?
If you name the button, you can connect to its onClick signal, or to a custom signal that it emits in onClicked. Example:
ApplicationWindow {
title: qsTr("Test Invoke")
width: 200
height: 100
Button {
signal messageRequired
objectName: "myButton"
y : 70
text : "About"
onClicked: messageRequired()
}
}
Note the signal in Button and the objectName property. Then the Python code just before exec could be for example:
def myFunction():
print 'handler called'
button = win.findChild(QObject, "myButton")
button.messageRequired.connect(myFunction)
button.clicked.connect(myFunction) # works too
Note that in the Button above, onClicked just emits the messageRequired signal, so it is better to drop the custom signal and connect to clicked directly. Both onClicked() and any slots connected to clicked will get called when you click button.
There is another solution which uses a Python model (QObject) in QML.
Python
engine = QQmlApplicationEngine()
engine.load("main.qml")
class Greeter(QObject):
#pyqtSlot(str)
def sayHello(self, name):
print("Hello, " + name)
ctx = engine.rootContext()
ctx.setContextProperty("greeter", Greeter())
QML
Button {
onClicked: {
greeter.sayHello("Alice")
}
}
Ref.
https://wiki.qt.io/Qt_for_Python/Connecting_QML_Signals