I have a QML-based app that loads a main.qml file from the file system like this:
myEngine->load("main.qml");
This works fine, but I'd like to "reload" the engine in case the main.qml was replaced with a newer version.
What I tried so far was calling load() again, assuming that the engine will automatically reset itself like in other Qt classes.
Unfortunately this is not the case. If I call the method again, another window will appear with the contents of the updated qml file, while the original window stays open and continues to display the old qml file.
To fix this I tried to call load(QUrl()), followed by clearComponentCache() and a final load call for the new file. This results in the same effect.
Any ideas how I can "properly" reload a QML engine while the application is running?
Just saw this, but if you're still trying to figure this out - it's a three step process, and you have some of it.
You MUST close window created by the QQmlApplicationEngine first. In my case I pulled the first root object off the QQmlApplicationEngine and cast it to QQuickWindow, then call close().
Now you can call clearComponentCache on the QQmlApplicationEngine.
This is what my window closing code does (note that I gave my main window an objectName)
QObject* pRootObject = in_pQmlApplicationEngine->rootObjects().first();
Q_ASSERT( pRootObject != NULL );
Q_ASSERT( pRootObject->objectName() == "mainWindow" );
QQuickWindow* pMainWindow = qobject_cast<QQuickWindow*>(pRootObject);
Q_ASSERT( pMainWindow );
pMainWindow->close();
The third step is, of course, to load your QML.
Later, I moved to creating a QQuickView window instead of QQmlApplicationEngine, so that I could just call clearComponentCache and then setSource (I didn't like the user seeing the UI window vanish and then re-appear.)
I would try storing myEngine as a pointer on the heap, and deleting it after calling quit(). Then you can reconstruct it to get the new version of the QML file.
If you don't want to do this for some reason (e.g. because you want to keep the window around or whatever), you might try using a Loader and loading the QML file that way. Your main.qml would then look something like this:
import QtQuick 2.0
Loader {
source: "changing.qml"
}
Whenever you detect that changing.qml has changed, just toggle the active property of Loader to trigger a reload of the file.
Using a file watcher:
main.py
DEBUG = True
class EntryPoint(qtc.QObject):
if DEBUG:
qmlFileChanged = qtc.Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.qml_engine = qqml.QQmlApplicationEngine()
self.qml_entry = str(PATHS.playground.resolve())
self.qml_engine.load(self.qml_entry)
if DEBUG:
qml_files = []
for file in glob.iglob('**/*.qml', root_dir=PATHS.QML, recursive=True):
qml_files.append(str((PATHS.QML / file).resolve()))
self.file_watcher = QFileSystemWatcher(self)
self.file_watcher.addPaths(qml_files)
self.file_watcher.fileChanged.connect(self.on_qml_file_changed)
if DEBUG:
#slot
def on_qml_file_changed(self) -> None: # pragma: no cover
self.qml_engine.clearComponentCache()
window: QQuickItem = self.qml_engine.rootObjects()[0]
loader: QQuickItem = window.findChild(QQuickItem, 'debug_loader')
qtc.QEventLoop().processEvents(qtc.QEventLoop.ProcessEventsFlag.AllEvents, 1000)
prev = loader.property("source")
loader.setProperty('source', "")
loader.setProperty('source', prev)
playground.qml
import QtQuick
import QtQuick.Controls.Material
Window {
id: root
width: 1200
height: 900
visible: true
flags: Qt.WindowCloseButtonHint | Qt.WindowMinimizeButtonHint | Qt.CustomizeWindowHint | Qt.WindowTitleHint
Material.theme: Material.Dark
Material.accent: Material.Cyan
Pane {
anchors.fill: parent
objectName: "_rootRect"
Loader{id: loader
objectName: "debug_loader"
anchors.fill: parent;
source:"anything.qml"
}
}
}
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 make these codes to have a better understanding for my question:
main.py
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
app = QGuiApplication([])
engine1 = QQmlApplicationEngine()
engine1.load("hello.qml")
engine2 = QQmlApplicationEngine()
engine2.load("hello.qml")
app.exec_()
hello.qml
import QtQuick.Window 2.14
import QtQuick.Controls 2.15
Window {
visible: true
Button {
text: "Button"
onPressed: Qt.quit()
}
}
After pressing the button, both windows are closed. This is not what I expected.
Explanation
That's the default behavior pointed out in the docs:
List of configuration changes from a default QQmlEngine:
Connecting Qt.quit() to QCoreApplication::quit()
Automatically loads
translation files from an i18n directory adjacent to the main QML
file.
Translation files must have "qml_" prefix e.g. qml_ja_JP.qm.
Translations are reloaded when the QJSEngine::uiLanguage /
Qt.uiLanguage property is changed.
Automatically sets an incubation
controller if the scene contains a QQuickWindow.
Automatically sets a
QQmlFileSelector as the url interceptor, applying file selectors to
all QML files and assets.
The engine behavior can be further tweaked
by using the inherited methods from QQmlEngine.
(emphasis mine)
So there are 2 alternatives:
remove the connection:
for engine in (engine1, engine2):
engine.disconnect()
Use QQmlEngine instead of QQmlApplicationEngine.
Proper Solution
Going to the background problem that is how to close the window when the user presses a button so you should not use Qt.quit() but instead invoke the close() method of the window:
import QtQuick.Window 2.14
import QtQuick.Controls 2.15
Window {
id: root
visible: true
Button {
text: "Button"
onPressed: root.close()
}
}
You should only use Qt.quit() if you want to exit the entire application.
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).
How does one display a QtQuick UI fileset (ui.qml and .qml) from a normal QML Window?
To display other QML windows from my parent Window class, the call is, roughly:
var component = Qt.createComponent("SubWindow.qml")
var window = component.createObject(window)
window.show()
I've tried calling both the QtQuickUISubwindow.ui.qml and QtQuickUISubwindow.qml, but neither works.
Are QtQuickUI files not meant to be sub windows?
Window and ApplicationWindow are not of Type Item (or QQuickItem). This however is a requirement for being placed as a root item in a .ui.qml-file as stated in the documentation:
The following features are not supported:
[...]
Root items that are not derived from QQuickItem or Item
So the answer is:
No, you can't use QtQuickUI files neither as windows nor as sub windows.
You can however easily use them within a sub window
// main.qml
Window {
id: root
width: 800
height: 600
visible: true
Loader { // creates my sub window without JS
id: winLoader
active: false // Change to create the window.
sourceComponent: Window {
width: srcLoader.item ? srcLoader.item.width : 0
height: srcLoader.item ? srcLoader.item.height : 0
visible: srcLoader.item
Loader {
id: srcLoader
source: "QtQuickUISubwindow.ui.qml" // Your ui.qml-file *here*
active: true
}
onClosing: winLoader.active = false
}
}
Button {
text: "Show sub window!"
onClicked: winLoader.active = true
}
}
This code has not been tested by me. Maybe I'll do so later once I have access to a Qt machine. How to initialize multiple windows with a repeater and a ListModel you can find here: https://stackoverflow.com/a/47018205/2056452
You might pass the source of the srcLoader to the model, and read it from there if you want to open multiple different windows.
You can ofc modify the QtQuickUISubwindow.qml-file and add a Window or ApplicationWindow of the appropriate size as the root element. Then you can create everything as you used to do, using JS - and hope that the garbage collector plays nicely.
I am creating dynamic component in QML as follows:
var component = Qt.createComponent("PlayerWindow.qml")
if (component.status != component.errorString())
console.log(component.errorString())
var playerWin = component.createObject(rootWindow);
Here rootWindow is my main application window. Now, the PlayerWindow is quite simple as:
Window {
id: playerWindow
width: parent.width
height: parent.height
Component.onCompleted: {
console.log(parent.width)
console.log(rootWindow.height)
}
}
The thing is that the values for parent.width and rootWindow.width are really different and this is also evident when the window is displayed. However, rootWindow is set as the parent in the createObject call. So, I am not sure what is happening there but I wanted to know if this is the correct way to set the component parent when they are being dynamically created.
Try to add console.log(parent) in the code. You will see something like qml: QQuickRootItem(0x1e3e4e0). If you check the Qt doc you will find that Item.parent() returns Item but Windows is not Itemdescendant but QQuickWindow. Also from documentation:
A QQuickWindow always has a single invisible root item ...
So, in your case parent and rootWindow are different objects.
P.S. The dynamic object creation in your code can produce an error since component.createObject will be executed although Qt.createComponent returns error. Just copy the code from Qt documentation.