I have a Qml component consisting of sub-components. After everything has been loaded (rendered) I want to perform some action.
Is there a way to find out when all components are loaded? Just using the Component.onCompleted event on the root element does not suffice because the children are not guaranteed to be loaded.
Regards,
You should be able to use the afterSynchronizing() signal of QQuickWindow to achieve this:
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
ApplicationWindow {
visible: true
width: 1280
height: 700
property bool initialised: false
onAfterSynchronizing: {
if (!initialised) {
print("initialising...");
// stuff...
initialised = true;
}
}
}
Pros:
You get it for free.
Cons:
You can only use it on QQuickWindow derivatives.
An alternative is to use a Loader; specifically its loaded() signal:
Loader {
source: "MyComponent.qml"
onLoaded: {
// stuff...
}
}
Pros:
A much clearer alternative to anyone who may have to maintain your code.
Works without having a Window; can use it at any level in your scene's "hierarchy".
Cons:
Comes with a little overhead. If the component is constructed often, at a low level (like a button), it may be worth profiling to see if it has a negative effect on performance. If not, it's negligible.
Related
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 written this Basic Qml file which imports QtTest but is not a test.
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtTest 1.1
ApplicationWindow {
id: window
width: 400
height: 250
visible: true
menuBar: MenuBar {
Menu{
title: "alpha"
MenuItem{ text: "print after 1 sec"; onTriggered:{printAfterDelay(1000)}}
}
}
Rectangle{
anchors.fill: parent
color: "red"
}
function printAfterDelay(delay){
wait(delay);
console.log("print")
}
}
Once I run it, it throws the error: ReferenceError: wait is not defined.
Does this function only work while running actual test cases or am I doing something wrong? And if it only runs with test cases, are there other alternatives (not including timer)?
PS: I'm trying to avoid timer because when the code becomes more complex and relies on multiple timers it sacrifices a lot of readability.
You are right, wait is defined only for test cases, so it will be visible only inside
TestCase {
However I disagree that using timers will reduce readability. Take a look at this answer:
How to create delay function in QML?
You define your delay function only once:
function delay(delayTime, cb) {
timer = new Timer();
timer.interval = delayTime;
timer.repeat = false;
timer.triggered.connect(cb);
timer.start();
}
and then you can use f.e. lambda:
delay(100, function() {
console.log("print")
})
It's only a few characters more than your code. Up to my knowledge there is no better solution in Qml.
wait() is a method of TestCase QML type. You can't use it outside a TestCase. Yous should use events sytem or signal/slots, instead.
Using delay isn't the good answer to increase readability: if your code becomes too complex, you must refactor it and split it in smaller components.
In my application I have a global system that handles navigation between "screens". In QML I can simply call something like:
appNavigation.show("MyScreen.qml", NavigationType.FADE)
this calls a C++ part of the code which handles the current stack of screens and uses signals to report back to QML to do the actual animation. At the end in QML some Loader will load the input qml ("MyScreen.qml" in this case) and show it as defined.
My issue here is how to inject data into newly loaded screen. Essentially I would like to do something like the following:
function showMyScreen() {
MyScreen screen = appNavigation.show("MyScreen.qml", NavigationType.FADE)
screen.someData = "some data here"
}
but is this possible? Could I somehow return the screen that is loaded by the loader?
I am guessing not so I would satisfy with sending the data with the navigation itself like:
function showMyScreen() {
MyScreen screen = appNavigation.show("MyScreen.qml", NavigationType.FADE, "some data here")
}
I could forward the data to the point where I set source to the loader but still what then? How or where would that specific screen that is going to be loaded get the data. To reduce is this is what I get:
function setNewItemWithData(newItem, data) {
loader.source = newItem
loader.concreteScreen.data = data // Not really doable
}
again I assume this is not doable and I need to forward the data down to loader and use onLoaded event. So what I would do is something like:
onLoaded: {
myLoadedScreen.data = data
}
I assume something like this is possible but how? What am I missing here, how do I get myLoadedScreen and how to access its properties?
What I am currently doing now is dumping the data in C++ part and then collecting it in the loaded QML. So like the following:
appNavigation.injectedData = "some data here"
and then in the newly loaded item:
property data = appNavigation.injectedData
It works but this seems like extremely poor coding. Any of the alternatives would be helpful.
Thank you for your patience.
Since the request for MCVE was made:
This is a general problem and I expect it to have multiple solutions. I would be looking forward to any of them. However the minimal example to produce this is creating a new project and adding a loader with another qml to which some property should be changed:
main:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Loader {
anchors.fill: parent; anchors.margins: 20
source: "MyScreen.qml"
// TODO: make screen green (loadedScreen.color = "green")
}
}
MyScreen:
import QtQuick 2.0
Rectangle {
color: "red"
}
Current result is seeing a red rectangle and desired result is to see a green one. The point being that the main screen needs to tell what color the loaded screen needs to use.
You have to use the item property of the Loader to get the object loaded:
Loader {
id: loader
anchors.fill: parent; anchors.margins: 20
source: "MyScreen.qml"
onLoaded: loader.item.color = "green"
}
To do that, you might as well use Component (If you use it when reacting to an event)
Component {
id: myScreenComponent
MyScreen {
anchors.fill: parent
}
}
function showMyScreen() {
myScreenComponent.createObject(this, {"color: "green"});
}
Alternatively, given your first code, I would recommend you to use StackView.
The push method seems to be similar to your appNavigation.show one.
You can give it an url, some properties, and a transition type (that you can customize).
For a long time I wondered about the existence of the ObjectModel, and now I found the only good point is the attached property ObjectModel.index
However I'd like to maximize it's use, by using this information only once an Object is added to this ObjectModel.
If I create custom, reusable Components, I might add the following lines
import QtQml.Models 2.2
.....
ObjectModel.onIndexChanged: { ... set properties in dependance of the index ... }
to use the index information.
Now I am seeking for a less invasive solution, where I do not have to prepare a Component to be possibly used in an ObjectModel, but where I can do this { ... set properties in dependance of the index ... } at the point in time, where I add the Item to the ObjectModel or once I use it in a ListView then.
What I am missing are some signals, such as inserted(int index) that I could handle, to set the properties. This would also give me the opportunity to use other external properties, than the index
Why? So I do not have to create Components as this:
RectangleThatCouldBeUsedInAObjectModelThatSetsWidthAndHeightAccordignly.qml:
Rectangle {
ObjectModel.onIndexChanged: {
width = Qt.binding(function() { return 100 * ObjectModel.index }
height = Qt.binding(function() { return 100 * ObjectModel.index }
}
}
but just use a Rectangle instead (stupid example)
An to ensure, that I do not have to import QtQml.Models 2.2 in every Component, that might in maybe 10% of their usecases be used in an ObjectModel.
I just want to use it with minimal prior knowledge needed.
In QtQuick 2 using the QtQuick Controls you can create complex desktop apps. However it seems to me that the entire UI must be declared and create all at once at the start of the app. Any parts that you don't want to use yet (for example the File->Open dialog) must still be created but they are hidden, like this:
ApplicationWindow {
FileDialog {
id: fileOpenDialog
visible: false
// ...
}
FileDialog {
id: fileSaveDialog
visible: false
// ...
}
// And so on for every window in your app and every piece of UI.
Now, this may be fine for simple apps, but for complex ones or apps with many dialogs surely this is a crazy thing to do? In the traditional QtWidgets model you would dynamically create your dialog when needed.
I know there are some workarounds for this, e.g. you can use a Loader or even create QML objects dynamically directly in javascript, but they are very ugly and you lose all the benefits of the nice QML syntax. Also you can't really "unload" the components. Well Loader claims you can but I tried it and my app crashed.
Is there an elegant solution to this problem? Or do I simply have to bite the bullet and create all the potential UI for my app at once and then hide most of it?
Note: this page has information about using Loaders to get around this, but as you can see it is not a very nice solution.
Edit 1 - Why is Loader suboptimal?
Ok, to show you why Loader is not really that pleasant, consider this example which starts some complex task and waits for a result. Suppose that - unlike all the trivial examples people usually give - the task has many inputs and several outputs.
This is the Loader solution:
Window {
Loader {
id: task
source: "ComplexTask.qml"
active: false
}
TextField {
id: input1
}
TextField {
id: output1
}
Button {
text: "Begin complex task"
onClicked: {
// Show the task.
if (task.active === false)
{
task.active = true;
// Connect completed signal if it hasn't been already.
task.item.taskCompleted.connect(onTaskCompleted)
}
view.item.input1 = input1.text;
// And several more lines of that...
}
}
}
function onTaskCompleted()
{
output1.text = view.item.output1
// And several more lines...
// This actually causes a crash in my code:
// view.active = false;
}
}
If I was doing it without Loader, I could have something like this:
Window {
ComplexTask {
id: task
taskInput1: input1.text
componentLoaded: false
onCompleted: componentLoaded = false
}
TextField {
id: input1
}
TextField {
id: output1
text: task.taskOutput1
}
Button {
text: "Begin complex task"
onClicked: task.componentLoaded = true
}
}
That is obviously way simpler. What I clearly want is some way for the ComplexTask to be loaded and have all its declarative relationships activated when componentLoaded is set to true, and then have the relationships disconnected and unload the component when componentLoaded is set to false. I'm pretty sure there is no way to make something like this in Qt currently.
Creating QML components from JS dynamically is just as ugly as creating widgets from C++ dynamically (if not less so, as it is actually more flexible). There is nothing ugly about it, you can implement your QML components in separate files, use every assistance Creator provides in their creation, and instantiate those components wherever you need them as much as you need them. It is far uglier to have everything hidden from the get go, it is also a lot heavier and it could not possibly anticipate everything that might happen as well dynamic component instantiation can.
Here is a minimalistic self-contained example, it doesn't even use a loader, since the dialog is locally available QML file.
Dialog.qml
Rectangle {
id: dialog
anchors.fill: parent
color: "lightblue"
property var target : null
Column {
TextField {
id: name
text: "new name"
}
Button {
text: "OK"
onClicked: {
if (target) target.text = name.text
dialog.destroy()
}
}
Button {
text: "Cancel"
onClicked: dialog.destroy()
}
}
}
main.qml
ApplicationWindow {
visible: true
width: 200
height: 200
Button {
id: button
text: "rename me"
width: 200
onClicked: {
var component = Qt.createComponent("Dialog.qml")
var obj = component.createObject(overlay)
obj.target = button
}
}
Item {
id: overlay
anchors.fill: parent
}
}
Also, the above example is very barebone and just for the sake of illustration, consider using a stack view, either your own implementation or the available since 5.1 stock StackView.
Here's a slight alternative to ddriver's answer that doesn't call Qt.createComponent() every time you create an instance of that component (which will be quite slow):
// Message dialog box component.
Component {
id: messageBoxFactory
MessageDialog {
}
}
// Create and show a new message box.
function showMessage(text, title, modal)
{
if (typeof modal === 'undefined')
modal = true;
// mainWindow is the parent. We can also specify initial property values.
var messageDialog = messageBoxFactory.createObject(mainWindow, {
text: text,
title: title,
visible: true,
modality: modal ? Qt.ApplicationModal : Qt.NonModal
} );
messageDialog.accepted.connect(messageDialog.destroy);
messageDialog.rejected.connect(messageDialog.destroy);
}
I think loading and unloading elements is not actual any more because every user have more than 2GB RAM.
And do you think your app can take more than even 512 MB ram? I doubt it.
You should load qml elements and don't unload them, no crashes will happens, just store all pointers and manipulate qml frames.
If you just keep all your QML elements in RAM and store their states, it will works faster and looks better.
Example is my project that developed in that way: https://youtube.com/watch?v=UTMOd2s9Vkk
I have made base frame that inherited by all windows. This frame does have methods hide/show and resetState. Base window does contains all child frames, so via signal/slots other frames show/hide next required frame.