Testing QML based app with pytest in python - python

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.

Related

How to emit Signal from nested QML page to python

in my QML/python app I can emit signal from main.qml to the python code. But now In main.qml I added StackLayout for loading another page1.qml. In that page1.qml is button, now I want to emit signal from this button to the python.
I use this method for emit signals from main.qml file to the python:
But do not know how to emit it from nested page1.qml
main.py
from PySide2.QtCore import QObject, QUrl, Slot, Signal, Qt
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
class Foo(QObject):
#Slot(str)
def test_slot(self, input_string : str):
print(input_string)
if __name__ == "__main__":
import os
import sys
app = QGuiApplication()
foo = Foo()
engine = QQmlApplicationEngine()
#CHANGES: line excluded engine.rootContext().setContextProperty("foo", foo)
qml_file = "main.qml"
current_dir = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(current_dir, qml_file)
engine.load(QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
#CHANGES: connect QML signal to Python slot
engine.rootObjects()[0].test_signal.connect(foo.test_slot, type=Qt.ConnectionType.QueuedConnection)
sys.exit(app.exec_())
main.qml
import QtQuick 2.13
import QtQuick.Controls 2.13
ApplicationWindow {
visible: true
//CHANGES: declare signal
signal test_signal(string input_string)
Button {
anchors.centerIn: parent
text: "Example"
//CHANGES: emit signal
onClicked: test_signal("Test string")
}
}
https://stackoverflow.com/a/69595659/5166312
Thank you very much.
Add objectName string property for nested pages (included the case if they are in separate files):
objectName: "page1_objname"
Access nested pages from Python (or C++) backend
qmlpage1 = engine.rootObjects()[0].findChild(QObject, "page1_objname")
Connect slot and signal as usually in Python or C++ backend (you already have code in your question or in my answer https://stackoverflow.com/a/69595659/5166312).
qmlpage1.page1_signal.connect(
test_object.test_slot3,
type=Qt.ConnectionType.QueuedConnection)
The whole project with StackLayout, two nested pages, 3 signals and 3 slots in my test example on https://github.com/Ornstein89/pyside6-qml-slotsignal.
main.py
# This Python file uses the following encoding: utf-8
import sys
from pathlib import Path
from PySide6.QtCore import QObject, QUrl, Slot, Signal, Qt
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
class TestClass(QObject):
'''
Object - slot-owner and signal-acceptor
'''
#Slot(str)
def test_slot1(self, input_string : str):
print(input_string)
#Slot(str)
def test_slot2(self, input_string : str):
print(input_string)
#Slot(str)
def test_slot3(self, input_string : str):
print(input_string)
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
test_object = TestClass()
qml_file = Path(__file__).resolve().parent / "main.qml"
engine.load(qml_file)
if not engine.rootObjects():
sys.exit(-1)
# !!! connect ApplicationWindow.mainapp_signal() to test_object.test_slot1
engine.rootObjects()[0]\
.mainapp_signal.connect(
test_object.test_slot1,
type=Qt.ConnectionType.QueuedConnection)
# !!! access nested page1 from python backend
qmlpage1 = engine.rootObjects()[0].findChild(QObject, "page1_objname")
# !!! and connect MyPage1.page1_signal() to test_object.test_slot2
qmlpage1.page1_signal.connect(
test_object.test_slot2,
type=Qt.ConnectionType.QueuedConnection)
# !!! access nested page2 from python backend
qmlpage2 = engine.rootObjects()[0].findChild(QObject, "page2_objname")
# !!! and connect MyPage2.page2_signal() to test_object.test_slot3
qmlpage2.page2_signal.connect(
test_object.test_slot3,
type=Qt.ConnectionType.QueuedConnection)
sys.exit(app.exec())
main.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
width: 480
height: 640
visible: true
title: qsTr("Example for Stackoverflow")
// !!! singal №1 in root QML Object - most easy to connect in main.py
signal mainapp_signal(string input_string)
StackLayout {
id : stacklayout
anchors.fill: parent
MyPage1 {
id: page1
}
MyPage2 {
id: page2
}
}
}
MyPage1.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
Page{
id: page1
// !!! singal №2 in nested QML Object
signal page1_signal(string input_string)
// !!! important - this name is used
// to access page1 from C++ or Python backend
objectName: "page1_objname"
Button{
text: "Go to page 2"
anchors.centerIn: parent
onClicked: {
stacklayout.currentIndex = 1
// call root object signal from nested object
mainapp_signal("mainapp_signal() from page1");
// call nested object signal from same object
page1_signal("page1_signal() from page1");
// call another nested object signal
page2.page2_signal("page2_signal() from page1");
}
}
}
MyPage2.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
Page{
id: page2
// !!! singal №3 in nested QML Object
signal page2_signal(string input_string)
// !!! important - this name is used
// to access page2 from C++ or Python backend
objectName: "page2_objname"
Button{
text: "Go to page 1"
anchors.centerIn: parent
onClicked: {
stacklayout.currentIndex = 0
}
}
}
You can add a Conections element to use signals of loaded component.
Look at documentation here.
Also, you can declare a component for your page1.qml item and use sourceComponent property of Loader. In this way you can use signal inside component to call an outer method. Look at documentation here.

How to use MapType in qml?

I am working on a gui project in python, where i want to show a map using pyqt.
I've stumbled upon a way of using qml to do just that, and I was able to show a simple map, but when I try customizing it's appearance using the activeMapType property, the program only show a blank white screen. (I dont have debug messages, cause I'm not using Qt creator)
I suspect a problem is in the declaration of "map_type", but i can't figure what it is.
The python code (simply loads the qml file):
import os
from PyQt5 import QtCore, QtWidgets, QtQuickWidgets, QtPositioning
from PyQt5.QtQuick import QQuickView
from PyQt5.QtWidgets import *
if __name__ == '__main__':
app = QApplication([])
w = QtQuickWidgets.QQuickWidget()
qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
w.setSource(QtCore.QUrl.fromLocalFile(qml_path))
w.setResizeMode(QtQuickWidgets.QQuickWidget.SizeRootObjectToView)
w.show()
app.exec()
Main.qml:
import QtQuick 2.11
import QtPositioning 5.11
import QtLocation 5.11
Rectangle {
id:rectangle
width: 640
height: 480
Plugin {
id: osmPlugin
name: "osm"
}
MapType {
id: map_type
description: "type of the map"
mobile: false
name: "test"
night: true
}
property variant locationTC: QtPositioning.coordinate(44.951, -93.192)
Map {
id: map
activeMapType: map_type
anchors.fill: parent
plugin: osmPlugin
center: locationTC
zoomLevel: 10
copyrightsVisible: false
}
}

Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode()

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"
}
}

QML does not play well with system theme [duplicate]

I have a problem with Toolbar when I use the qml file with PyQt5. The result is not the seem : no background image when mouse is over, image no resize automatically.
I want to know if it's normal.
How can I do for have the same result with PyQt5
The result with qmlscene:
The result with Python:
Thanks you for your help.
File : _test.py
from PyQt5.QtCore import (
pyqtProperty,
pyqtSignal,
pyqtSlot,
QAbstractListModel,
QModelIndex,
QObject,
Qt,
QTimer,
)
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtQuick import QQuickView
class MainWindow(QObject):
def __init__(self, parent=None):
super().__init__(parent)
if __name__ == "__main__":
import sys
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
main_window = MainWindow()
engine.load("_test.qml")
if not engine.rootObjects():
sys.exit(app.exec_())
sys.exit(app.exec())
File : _test.qml
import QtQuick 2.4
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.3
import QtQuick.Controls.Styles 1.3
ApplicationWindow {
width: 500
height: 200
visible: true
ToolBar {
Layout.fillWidth: true
RowLayout {
anchors.fill: parent
ToolButton {
//width: parent.height
anchors.margins: 4
iconSource: "ico/abacus.png"
}
ToolButton {
width: parent.height
Image {
source: "ico/quitter.png"
anchors.fill: parent
anchors.margins: 4
}
}
ToolButton {
width: parent.height
iconSource: "ico/start.png"
anchors.margins: 4
}
ToolButton {
width: parent.height
Image {
source: "ico/stop.png"
anchors.fill: parent
anchors.margins: 4
}
}
}
}
}
Analyzing the source code of qmlscene and testing with the --apptype option I get the following:
qmlscene _test.qml --apptype gui
qmlscene _test.qml --apptype widgets
So analyzing the fundamental difference is that QApplicacion is being used and not QGuiApplication, so internally it should activate some flag that scales the icons.
Considering the above, the solution is:
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import QQmlApplicationEngine
if __name__ == "__main__":
import os
import sys
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
current_dir = os.path.dirname(os.path.realpath(__file__))
file = os.path.join(current_dir, "_test.qml")
engine.load(QUrl.fromLocalFile(file))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
According to the docs of Qt Quick Controls 1:
Note: We are using QApplication and not QGuiApplication in this
example. Though you can use QGuiApplication instead, doing this will
eliminate platform-dependent styling. This is because it is relying on
the widget module to provide the native look and feel.
So it seems that the scaling of the icons is part of the style of the platform.
Each type of project requires a QXApplication:
Console application: You can use any of the 3 types of QXApplication, but using QCoreApplication is the most optimal since the other QXApplication require that they have a window system that in that case is an unnecessary requirement.
QML Application: It requires at least one QGuiApplication, but for certain ones such as the need to use the styles of each platform it is necessary to use QApplication.
Qt Widgets Application: A QApplication is necessary because QWidgets use the styles of each platform.
The fact that sizes change, is this a problem of QtQuick.Controls 1?
Yes, one of the main differences between QQC1 and QQC2 is that the first one is developed to support desktop platforms so you use the styles, unlike the second one that is designed for embedded systems where the main objective is performance. For more information read Differences with Qt Quick Controls 1
Conclusions:
If you want your GUI made with QML to respect the styles of your desktop platform then you must use QQC1 with QApplication.
If your goal is that the style of your application does not respect the style of the desktop in addition to wanting more performance you should use QQC2 with QGuiApplication.

How to bind buttons in Qt Quick to Python PyQt 5

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

Categories