PDA

View Full Version : Qt 5.4 - Scriptable Qml Application



motaito
14th April 2015, 15:35
Hi all

I want to learn how to make a qml application scriptable. I would like to have user defined scripts that can be changed without having to recompile the app. Also, I would like to create an object and call a function from anywhere in javascript. During researching the topic I found some webpages mentioning that QtScript is no longer maintained. Is that true? What is the recommanded way to make a qml-application fully scriptable in qt5.4 (for qml applications)?


Some background:
At first I thought, that there is already a javascript functionality in qml. So, I started to write some javascripts and loaded them from a file. I can make my c++ objects available with:

// in main.cpp
qmlRegisterType<CustonObject>("MyCustonObject", 1, 0, "QmlCustonObject");

// In my qml I can use a script with
import "someFile.js" as File
This works great as long as the file is in the resources (qrc). However, I want to be able to change the scripts without having to recompile the application. So, I looked at

Qt.import("otherFile.js")
And wanted to import that file from the "someFile.js" which is in the resources. I found that I need an absolute path for this to work. The problem is then that the path will change across different platforms. So I needed a way to have the path dynamic. The only way here was to wrap the import in a function

// function in "someFile.js"
function loadScript(path)
{
Qt.import(path);
}
While that works, the script is then only available inside the function scope. If I warp everything in such a function call, the script gets laoded every time the function is called. So, I packed it into an object instead to avoid loading the script multiple times.

// defined in "someFile.js"
function Loader() {
Qt.import(getPathFromQmlFunction());
}
Loader.prototype.Execute() {
// do stuff here...
}

var MyLoader = new Loader();
In a qml file I can then add a helper function. The only part to be aware of is that this function must be declared before the "someFile.js" gets included.

// defined in qml
function getPathFromQmlFunction()
{
// figure out path for plattform
return "some/path/goes/here/otherFile.js";
}
while this works, it requires me to fiddle with the Loader object and extend it depending on what I want to make available to the scripts. It seams a cumbersome way to implement scripting and I am unsure about it's limitations and restrictions across platforms. Hence, my initial question. What is the proper/recommanded way to make a qml application fully scriptable? Should I continue with this approach or should I use Qt Script instead and load my scripts with a QScriptEngine?

Edit: fixed some typo

wysota
14th April 2015, 18:37
QML already is scriptable. You just need to get the paths right. At worst expose a C++ interface that will execute your scripts on the QML engine.

motaito
14th April 2015, 19:26
Thanks for the reply!

I am not sure I got your point. As I pointed out in the background info, it does work. I am not sure if it's good practice. Because it depends on the order in which the elements are created and I don't know if there are differences across platforms at what point in time qml's are created. I figured it should be possible to make some javascripts available and use them without a wrapper class (the Loader class I mentioned earlier). However, I can not load such files unless they are included in the resources. That is not what I am looking for. I want to be able to adjust the javascripts without having to recompile the app. E.g. fix a bug extend functionality through scripts etc.

Also, does that mean that QtScript is indeed no longer maintained? Otherwise I might be able to load the scripts with a QScriptEngine instead which could solve the path problem. But if Qml is already scriptable and QtScript is not maintained anymore I would essentially add a second script engine that lacks support later on. So, I am still not sure about the best practice here.

motaito
14th April 2015, 21:47
Ok, I think I just realized what you mean... I got so caught up in over thinking the issue that I overlooked a QQmlApplicationEngine has it's own evaluate function...

What I haven't figured out yet is, how to call a function that is defined in an external javascript from qml. E.g.


// in main.cpp
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

QFile file("../path/to/file.js");
file.open(QIODevice::ReadOnly);
engine.evaluate(scriptFile.readAll(), "file.js");
file.close();


// in qml
...
MouseArea {
onClicked: {
var test = fromExternalJS(); // this doesn't work
console.log(test);
}
}
...


// in file.js
function fromExternalJS()
{
return "testing...";
}

I am not sure how to do this. I assume that's what you referred to by
At worst expose a C++ interface that will execute your scripts on the QML engine.Sorry, I am new to this. Maybe I am using the wrong search terms and so I can't find an example online. Does anyone have a link to where I can find the answer, or maybe present a code snippet?

Any input is greatly appreciated.

wysota
14th April 2015, 23:41
However, I can not load such files unless they are included in the resources.
That's what I meant about getting the paths right. You can use any file you want anywhere ("not in the resource system") as long as the engine knows how to access it (e.g. similar to Qt.resolvePath()). You need to make entry points where scripts can hook up.


Also, does that mean that QtScript is indeed no longer maintained?
Indeed. You couldn't merge it with QML anyway.


Otherwise I might be able to load the scripts with a QScriptEngine instead which could solve the path problem.
You can do that with QJSEngine if you want however those scripts would be totally separated from your Qt Quick environment.

We cannot provide any concrete help without you specifying what you mean by an application being "scriptable". I don't think letting the user execute arbitrary scripts on your engine is a good idea, by the way.

motaito
15th April 2015, 02:27
Thanks for your help!

That's what I meant about getting the paths right. You can use any file you want anywhere ("not in the resource system") as long as the engine knows how to access it (e.g. similar to Qt.resolvePath()). You need to make entry points where scripts can hook up.
I don't know how to dynamically adjust a path in an import statement, which would be necessary to have the app working on different platforms (or different machines for that matter) as the path needs to be absolute if they are not embedded in a resource.


You can do that with QJSEngine if you want however those scripts would be totally separated from your Qt Quick environment.That's a problem because I need to update the qml after calling a function in javascript. They need to be connected.

I can't wrap my head around scripting... I don't want to start learning QScriptEngine if it's no longer maintained. However, QJSEngine isn't doing it for me either :(

consider this:


// in foo.js
function fooTest()
{
return "some text";
}


// in main.cpp
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));


QFile scriptFile("foo.js");
scriptFile.open(QIODevice::ReadOnly);
QTextStream s(&scriptFile);
QString content = s.readAll();
scriptFile.close();
engine.evaluate(content, "foo.js");

QJSValue aa = engine.globalObject().property("fooTest");
qDebug() << aa.call().toString(); // output: "undefined"

QJSEngine eng;
eng.evaluate(content);
QJSValue bb = eng.globalObject().property("fooTest");
qDebug() << bb.call().toString(); // output: "some text"

return app.exec();
}


Why is there a different output and why does it not work when I use the evaluate function from QQmlApplicationEngine? Does QQmlApplicationEngine implement a different QJSEngine? That would not make sense at all, the return type for both evaluate functions is the same which indicates they use the same type of QJSEngine.

Also, by loading files that way I seam to loose a lot of functionality. E.g. I can not use Qt.import() in a file loaded like this nor does it work to make an instance of c++ class (var c = new MyClass(); will not work in javascript).


further details:
I try to explain my actual issue in the app. I have a network manager that grabs content form html pages. I then filter out the part that I want and use it further. However, parsing the html response differs for each page. I might add other pages later. Also, if a page gets redesigned I would need to adjust the parsing function. So, I want to be able to add parsing functions later or change them if needed, without having to recompile the app. Instead I want to be able to add parsing functions in javascript.

I do have a working solution but it leaves me with the impression that it's far more complicated then it should be. Anything that I read about scripting did not work and I had to fiddle for 3 days to find something that works. Here is an overview of what I am doing, in case it helps. This is the only way I could make it work completely. By that I mean call javascript functions from qml, dynamically include javascript files and update qml from javascript files etc. It seams like a hack to me. I am looking for a better, cleaner solution.



details (only for the eager reader :))


// in main.cpp
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

qmlRegisterType<NetworkManager>("MyNetworkManager", 1, 0, "NetworkManagerHandler");

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

return app.exec();
}



// in main.qml
Window {
...
// component with the actual layout
MainLayout {
id: layoutMain
}
...

// must be defined in hierarchy above where Builder is created
function getIncludePaths()
{
// adjust due to platform (I exclude the path building function to keep it more clear)
var path =["file:///path/goes/here/defaultBuildFunction.js",
"file:///path/goes/here/buildDefinitions.js"];
return path;
}
}



// in mainLayout.qml -> Builder is created here
import "fileBuildScript.js" as FileBuildScript
...
MouseArea {
...
onClicked: {
FileBuildScript.build("someIdentifier");
}
}
...



// in fileBuildScript.js
Qt.include("networkManagerScript.js"); // from resource file -> explained further down

var BuilderClass = function()
{
var path = getIncludePaths(); // function from main.qml
for(var i=0; i < path.length; ++i)
Qt.include(path[i]); // from external file

this.buildDefinitions = buildDefs;
}
BuilderClass.prototype.build = function(identifier)
{
var index = 0; // rest ommited to keep it clear -> would find index based on identifier
this.buildDefinitions[index].buildFunction();
}

var Builder = new BuilderClass();


// wrapper function to call build from Builder instance in mainLayout (for conveniance)
function build(identifier)
{
Builder.build(identifier);
}



// in file:///path/goes/here/buildDefinitions.js -> external file that the I can adjust without recompiling the app
.pragma library

var buildDefs = [
{
name: "someIdentifier",
buildFunction: buildNormal // function pointer -> function defined in defaultBuildFunction.js
},
{
name: "otherIdentifier",
buildFunction: buildSpecial // function pointer -> function defined in defaultBuildFunction.js
}
];



// in file:///path/goes/here/defaultBuildFunction.js -> external file that the I can adjust without recompiling the app
.pragma library

function buildNormal()
{
function reply(text)
{
text = "buildNormal"; // ommited parsing to keep it clear
updateElement(text); // update in qml element
updateGUI(); // update in qml element
}

var m = new NetworkMangerScriptable();
m.fetch(reply, "http://www.somepage.com");
}
...


finally I have to define the network manager...


// in networkmanager.h
#ifndef NETWORKMANAGER_H
#define NETWORKMANAGER_H

#include <QObject>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>

class NetworkManager : public QObject
{
Q_OBJECT
public:
NetworkManager(QObject *parent=0);
Q_INVOKABLE void fetch(const QString& url);

public slots:
void replyFinished(QNetworkReply*);

signals:
void reply(const QString& reply);

private:
QNetworkAccessManager* m_NetworkManager;
};

#endif // NETWORKMANAGER_H



// in networkmanager.cpp
#include "networkmanager.h"

NetworkManager::NetworkManager(QObject *parent)
: QObject(parent)
{
m_NetworkManager = new QNetworkAccessManager(this);

connect(m_NetworkManager, SIGNAL(finished(QNetworkReply*)),
this, SLOT(replyFinished(QNetworkReply*)));
}

void NetworkManager::fetch(const QString& url)
{
m_NetworkManager->get(QNetworkRequest(QUrl(url)));
}

void NetworkManager::replyFinished(QNetworkReply* pReply)
{
QByteArray data = pReply->readAll();
QString str(data);

//process str -> in my case I hook up the javascript to the signal
emit reply(str);
}


// in NetworkManager.qml (necessary component to make an instance in javascript)
import QtQuick 2.4
import MyNetworkManager 1.0

NetworkManagerHandler {
id: networkManager
}


// in networkManagerScript.js (so I can make an instance in a javascript e.g. var m = new NetworkMangerScriptable();)
.pragma library

var NetworkMangerScriptable = function()
{
this.Manager = this.init();
this.currentReplyFunction = function(text){}; // dummy definition
};

NetworkMangerScriptable.prototype.init = function()
{
var objManager;

var component = Qt.createComponent("../../component/network/NetworkManagerQml.qml");
objManager = component.createObject(mainLyout);
if(null == objManager)
console.log("Error: failed to create network manager.");

return objManager;
};

NetworkMangerScriptable.prototype.fetch = function(replyFunction, uri)
{
try { this.Manager.reply.disconnect(this.currentReplyFun ction); }
catch(e) { console.log("Error: " + e); }

this.currentReplyFunction = replyFunction;
this.Manager.reply.connect(this.currentReplyFuncti on);

this.Manager.fetch(uri);
};

anda_skoa
15th April 2015, 08:05
Also, does that mean that QtScript is indeed no longer maintained?

QtScript is part of the Qt5 release, so of course it is maintained at least until the end of the version 5 series.
It is not actively developed any further, so no new functionality will be added in any of the new releases.

QJSEngine is under active development and might at some point gain the same capabilities as QScriptEngine, but the current focus is on the needs of QQmlEngine.
Given that you have a working solution it seems that you don't need any of the features from QScriptEngine.

There is quite a lot of things that QQmlEngine can do, see http://code.woboq.org/qt5/qtdeclarative/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp.html for inspiration.

One option for you might be to load a QML file and let it import the JavaScript like you would do for the main application itself.

Cheers,
_

wysota
15th April 2015, 08:05
I don't know how to dynamically adjust a path in an import statement,
Not in an import statement. I have no idea what sense would it make. You should provide a function similar to Qt.resolvePath() that will resolve paths relative to your scripts directory.


That's a problem because I need to update the qml after calling a function in javascript. They need to be connected.

I can't wrap my head around scripting... I don't want to start learning QScriptEngine if it's no longer maintained. However, QJSEngine isn't doing it for me either :(
QScriptEngine is of no use to you. You cannot interconnect two script engines this way.


consider this:


// in foo.js
function fooTest()
{
return "some text";
}


// in main.cpp
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));


QFile scriptFile("foo.js");
scriptFile.open(QIODevice::ReadOnly);
QTextStream s(&scriptFile);
QString content = s.readAll();
scriptFile.close();
engine.evaluate(content, "foo.js");

QJSValue aa = engine.globalObject().property("fooTest");
qDebug() << aa.call().toString(); // output: "undefined"

QJSEngine eng;
eng.evaluate(content);
QJSValue bb = eng.globalObject().property("fooTest");
qDebug() << bb.call().toString(); // output: "some text"

return app.exec();
}


Why is there a different output and why does it not work when I use the evaluate function from QQmlApplicationEngine? Does QQmlApplicationEngine implement a different QJSEngine? That would not make sense at all, the return type for both evaluate functions is the same which indicates they use the same type of QJSEngine.
In the first case your script is probably executed in a local context so that you cannot modify the global context.


Also, by loading files that way I seam to loose a lot of functionality. E.g. I can not use Qt.import() in a file loaded like this nor does it work to make an instance of c++ class (var c = new MyClass(); will not work in javascript).
You didn't answer my previous question -- what exactly do you want to "script"? Because for now it looks like you want to allow the user to define some functions which are going to be called from your main program. This implies those functions would have to have hard-coded names and without providing them the application would not work. That's hardly what I'd call "scripting". Of course you can still do it through the asset system.



further details:
I try to explain my actual issue in the app. I have a network manager that grabs content form html pages. I then filter out the part that I want and use it further. However, parsing the html response differs for each page. I might add other pages later. Also, if a page gets redesigned I would need to adjust the parsing function. So, I want to be able to add parsing functions later or change them if needed, without having to recompile the app. Instead I want to be able to add parsing functions in javascript.
I don't see the problem. Prepeare a C++ function that will read the script from file, execute it on the qml engine and assign the result to a context property in the engine. Then whenever your main application wants to call the function, retrieve the context and execute the function.


function getIncludePaths()
{
// adjust due to platform (I exclude the path building function to keep it more clear)
var path =["file:///path/goes/here/defaultBuildFunction.js",
"file:///path/goes/here/buildDefinitions.js"];
return path;
}
}
Seems like a job for the asset system rather than manually building paths in javascript.


var BuilderClass = function()
{
var path = getIncludePaths(); // function from main.qml
for(var i=0; i < path.length; ++i)
Qt.include(path[i]); // from external file

this.buildDefinitions = buildDefs;
}
BuilderClass.prototype.build = function(identifier)
{
var index = 0; // rest ommited to keep it clear -> would find index based on identifier
this.buildDefinitions[index].buildFunction();
}

var Builder = new BuilderClass();


// wrapper function to call build from Builder instance in mainLayout (for conveniance)
function build(identifier)
{
Builder.build(identifier);
}



// in file:///path/goes/here/buildDefinitions.js -> external file that the I can adjust without recompiling the app
.pragma library

var buildDefs = [
{
name: "someIdentifier",
buildFunction: buildNormal // function pointer -> function defined in defaultBuildFunction.js
},
{
name: "otherIdentifier",
buildFunction: buildSpecial // function pointer -> function defined in defaultBuildFunction.js
}
];



// in file:///path/goes/here/defaultBuildFunction.js -> external file that the I can adjust without recompiling the app
.pragma library

function buildNormal()
{
function reply(text)
{
text = "buildNormal"; // ommited parsing to keep it clear
updateElement(text); // update in qml element
updateGUI(); // update in qml element
}

var m = new NetworkMangerScriptable();
m.fetch(reply, "http://www.somepage.com");
}
...
That's all very "not-declarative".

motaito
15th April 2015, 19:24
@anda_skoa
Thanks for the additional input and the link. If QtScript is no longer under development, I probably wont look at it further, especially if QQmlEngine can do the job as well. Even if it's still maintained. I basically consider it deprecated and prefer to learn the newer way of doing things.


@wysota

Not in an import statement. I have no idea what sense would it make. You should provide a function similar to Qt.resolvePath() that will resolve paths relative to your scripts directory.
If I have an external javascript file, I must provide an absolute link. So I am facing this problem

// inside some qml
import "file:///home/user/someDirectory/fileBuildScript.js" as FileBuildScript
...
MouseArea {
...
onClicked: {
FileBuildScript.build("someIdentifier");
}
}
If I now compile the app on windows instead of linux, the absolute path will no longer be correct. Thus It won't run on windows. Furthermore, the path would change depending on the directory in which the app is installed. So, How would I properly load an external javascript file?


In the first case your script is probably executed in a local context so that you cannot modify the global context.I must be missing something obvious because whatever I try it does not work. Can you provide a code snippet to illustrate how to execute javascript on the QQmlApplicationEngine?

What I am looking for is to load an external javascript file e.g.

// someFile.js
function foo()
{
return "foo got called.";
}

function bar()
{
return "bar got called.";
}
And then execute these functions from qml. As I understand, I would need to wrap this into a class of my own and declare it in main.cpp.

// main.cpp
qmlRegisterType<MyClass>("MyClass", 1, 0, "MyClassQml");
How would MyClass have to execute the loaded javascript?

Ultimately my goal it this. My network manager has an async function call. So, I would need these components.

// inside some qml
...
MouseArea {
...
onClicked: {
MyClass.build(); // external javascript gets called here.
}
}

Text {
id: myText
text: "This will be updated."
}
...
function updateGUI(text)
{
myText.text = text;
}


// inside external js file
function build()
{
function reply(text)
{
updateGUI(text); // call function from qml
}

var m = new NetworkManager(); // create instance form c++ class in javascript
m.fetch(reply, "http://www.somePage.com");
}
It would be a great help if you could provide a code snippet to illustrate how MyClass would look like. It doesn't need to be a complete implementation. Just the relevant parts of MyClass should suffice. I would greatly appreciate your effort!

wysota
15th April 2015, 20:49
If I have an external javascript file, I must provide an absolute link. So I am facing this problem

// inside some qml
import "file:///home/user/someDirectory/fileBuildScript.js" as FileBuildScript
...
MouseArea {
...
onClicked: {
FileBuildScript.build("someIdentifier");
}
}
This does not make much sense since you would need to know the path upfront. You can do this though:

MouseArea {
onClicked: executeHook("build", "someIdentifier")
}

Where executeHook() is a C++ function calling script "build" with parameter "someIdentifier". How to get script "build" is the responsibility of executeHook() function. Since the function is written in C++, it can access all kind of useful stuff, starting with files at arbitrary paths, ending on the script engine itself.


If I now compile the app on windows instead of linux, the absolute path will no longer be correct. Thus It won't run on windows. Furthermore, the path would change depending on the directory in which the app is installed. So, How would I properly load an external javascript file?
For the sake of answering the question -- you use the assets system.



What I am looking for is to load an external javascript file e.g.

// someFile.js
function foo()
{
return "foo got called.";
}

function bar()
{
return "bar got called.";
}
And then execute these functions from qml. As I understand, I would need to wrap this into a class of my own and declare it in main.cpp.

// main.cpp
qmlRegisterType<MyClass>("MyClass", 1, 0, "MyClassQml");
How would MyClass have to execute the loaded javascript?
Not sure what you mean.


Ultimately my goal it this.
Hurray, finally we'll know what the actual problem is :)


My network manager has an async function call. So, I would need these components.

// inside some qml
...
MouseArea {
...
onClicked: {
MyClass.build(); // external javascript gets called here.
}
}

Text {
id: myText
text: "This will be updated."
}
...
function updateGUI(text)
{
myText.text = text;
}


// inside external js file
function build()
{
function reply(text)
{
updateGUI(text); // call function from qml
}

var m = new NetworkManager(); // create instance form c++ class in javascript
m.fetch(reply, "http://www.somePage.com");
}
This seems much more declarative:

// inside some qml
...

Component {
id: builder
url: getPathToScriptFile() // <--- your "script" URL goes here
}

MouseArea {
...
onClicked: {
var obj = builder.createObject(myText)
obj.ready.connect(function(txt) { myText.text = txt; obj.destroy() })
obj.build()
}
}

Text {
id: myText
text: "This will be updated."
}

and the "script" file:
import "....."

QtObject {
id: root
signal ready(string txt)

function build() {
nm.fetch("http://www.somePage.com")
}

NetworkManager {
id: nm

onReply: root.ready(txt)
}

}

motaito
15th April 2015, 23:05
Where executeHook() is a C++ function calling script "build" with parameter "someIdentifier". How to get script "build" is the responsibility of executeHook() function. Since the function is written in C++, it can access all kind of useful stuff, starting with files at arbitrary paths, ending on the script engine itself.That's exactly my problem. I can not for the live of me figure out how the c++ function calls the script "build". It can't possible be that difficult. It is driving me completely nuts!


// in foo.js
function funcA()
{
return "funcA called.";
}

function funcB()
{
return "funcB called.";
}



// in main.cpp
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));


QFile scriptFile("foo.js");
scriptFile.open(QIODevice::ReadOnly);
QTextStream s(&scriptFile);
QString content = s.readAll();
scriptFile.close();

engine.evaluate(content, "foo.js");
// how do I call funcA() here ???


// Because by creating a separate QJSEngine this works!
QJSEngine eng;
eng.evaluate(content, "foo.js");
QJSValue val = eng.globalObject().property("funcA");
qDebut() << val.call().toString();

return app.exec();
}


For the sake of answering the question -- you use the assets system.I would love to read up on the asset system. I have been google-ing for "QT asset system" for hours now and can't find anything online... Maybe you have a link?

Thanks a lot for the code snippet! Not in a million years would I have gotten to that solution based on the Qt-documentation. I will try to implement it and see how it works.

Do I understand correctly, that you have to write the JavaScript file as if you write a component in qml rather than "normal" JavaScript code?

That would mean QQmlApplicationEngine behaves different than QJSEngine. Even though the documentation suggests that QQmlApplicationEngine inherits from QJSEngine. If it truly inherits, should it then not be usable in the exact same way (interpreting JavaScript code rather than qml declarative code)?

Sorry for being a pain in the butt. Somehow it just doesn't click in my brain. I am having a hard time finding information online or making the transfer from reading the Qt-documentation into actual code. I have to figure this out so I can sleep again :) I do understand that it takes up your time and effort. If you provide say a bitcoin address, I will make up for it by making a donation ;)

Added after 44 minutes:

This just added to the confusion :(


Component {
id: builder
url: getPathToScriptFile() // <--- your "script" URL goes here
}

This adds an externally defined qml component. Not a JavaScript right?

I still don't know how to execute a function from a JavaScript like in my code snippet from the previous post... I need to figure out how to do this part

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));

engine.evaluate(content, "foo.js");
// how do I call funcA() here ???

wysota
16th April 2015, 00:23
That's exactly my problem. I can not for the live of me figure out how the c++ function calls the script "build". It can't possible be that difficult. It is driving me completely nuts!
I don't understand what is the problem here. At some point your C++ code will "load" the script and store its functions in some variables. Calling the script is as simple as executing QJSValue::call() on it.



engine.evaluate(content, "foo.js");
// how do I call funcA() here ???


QJSValue myFunction = engine.evaluate(content, "foo.js"); // with foo.js containing "function() { ... }"
myFunction.call();


I would love to read up on the asset system. I have been google-ing for "QT asset system" for hours now and can't find anything online... Maybe you have a link?
It's late today, come back tomorrow :) And seriously this was introduced in Qt 5.4 or 5.3. It allows to use separate versions of the same file on different platforms. As for a link, see the first sentence :)


Do I understand correctly, that you have to write the JavaScript file as if you write a component in qml rather than "normal" JavaScript code?
It depends on the effect you want to achieve. Since you are already in a declarative environment it would be a sin not to use its power.


That would mean QQmlApplicationEngine behaves different than QJSEngine. Even though the documentation suggests that QQmlApplicationEngine inherits from QJSEngine. If it truly inherits, should it then not be usable in the exact same way (interpreting JavaScript code rather than qml declarative code)?
No idea what you mean. The behavior of both is the same as far as JavaScript is concerned.


If you provide say a bitcoin address, I will make up for it by making a donation ;)
You can donate to Qt Centre Foundation to help maintain this site if you really have to.


This just added to the confusion :(


Component {
id: builder
url: getPathToScriptFile() // <--- your "script" URL goes here
}

This adds an externally defined qml component. Not a JavaScript right?

Yes, of course. That's the whole point of my snippet.


I still don't know how to execute a function from a JavaScript like in my code snippet from the previous post... I need to figure out how to do this part

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));

engine.evaluate(content, "foo.js");
// how do I call funcA() here ???

You are stuck on trying to fix your wheelchair while a brand new Ferrari waits just besides you.

motaito
16th April 2015, 01:56
I don't understand what is the problem here. At some point your C++ code will "load" the script and store its functions in some variables. Calling the script is as simple as executing QJSValue::call() on it.No, it's not that simple. I found that I have to wrap the function in parenthesis for it to work... Sorry, but that is far from obvious. Also, that way I can only have one single function in one script. I can also not have another script that calls the function from the first script. It's hardly behaving like JavaScript.

using QQmlApplicationEngine

// main.cpp
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));

QJSValue vA = engine.evaluate("function a() { return "a"; }");
QJSValue vB = engine.evaluate("function b() { return a() + " b"; }");
QJSValue vC = engine.evaluate("(function c() { return "c"; })");
QJSValue vD = engine.evaluate("(function d() { return c() + " d"; })");

qDebug() << vA.call().toString(); // output: undefined
qDebug() << vB.call().toString(); // output: undefined
qDebug() << vC.call().toString(); // output: "c"
qDebug() << vD.call().toString(); // output: undefined
using QJSEngine

// main.cpp
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));

QJSEngine eng;
eng.evaluate("function a() { return "a"; }");
eng.evaluate("function b() { return a() + " b"; }");
eng.evaluate("(function c() { return "c"; })");
eng.evaluate("(function d() { return c() + " d"; })");

QJSValue vA = eng.globalObject().property("a");
QJSValue vB = eng.globalObject().property("b");
QJSValue vC = eng.globalObject().property("c");
QJSValue vD = eng.globalObject().property("d");

qDebug() << vA.call().toString(); // output: "a"
qDebug() << vB.call().toString(); // output: "a b"
qDebug() << vC.call().toString(); // output: undefined
qDebug() << vD.call().toString(); // output: undefined
That is neither the same usage nor the same result. Therefore my statement QQmlApplicationEngine and QJSEngine behave differently!

But if QQmlApplicationEngine is inherited by QJSEngine that means QJSEngine is actively developed. So, I guess I could just add a QJSEngine to my app and get it to work that way. It seams a bit strange though as QQmlApplicationEngine should already provide the needed functionality.

It's late today, come back tomorrowOf course, no problem. Take your time :) I am glad you help at all and I don't take it for granted!

As for a link, see the first sentenceI must be blind... I rechecked every first sentence from every paragraph you wrote. I find no further info on the asset system nor any link to it. The only link you provided is to QScriptEngine. But never mind. I want to focus on the scripting fist.

There is also an issue with the example you provided. It seams less intuitive to adjust the build function.

In my initial solution this would work.

function build()
{
function reply(text)
{
var t = parse(text);
updateGUI(t);
}

var m = new NetworkManager();
m.fetch("http://www.somePage.com");
}
I could change the function to this and add more info without having to adjust the app and recompile:

function build()
{
function reply(text)
{
var t = parse(text);

function innerReply(text)
{
var tInner = t + parseAdditionalInfoFromOtherWebsite(text);
updateGUI(tInner);
}

var m2 = new NetworkManager();
m2.fetch("http://www.someOtherPage.com");
}

var m = new NetworkManager();
m.fetch("http://www.somePage.com");
}
In my mind this is much easier to maintain than qml components. But maybe that's because I haven't really figured out how to use components to the fullest of their potential. I am still new to qml.


You are stuck on trying to fix your wheelchair while a brand new Ferrari waits just besides you.I guess I haven't figured out how to really drive a Ferrari yet :) I will play more with the sample you provided and give you some feedback. However, I also must sleep a bit and go to work tomorrow :)

Edit: fixed some typos

wysota
16th April 2015, 09:20
No, it's not that simple. I found that I have to wrap the function in parenthesis for it to work... Sorry, but that is far from obvious. Also, that way I can only have one single function in one script. I can also not have another script that calls the function from the first script. It's hardly behaving like JavaScript.
It is behaving exactly like JavaScript because it is Javascript. What you call "difference" is probably that your code is executed in different contexts as QML probably sandboxes its environment to prevent breaking it by an irresponsible script.


But if QQmlApplicationEngine is inherited by QJSEngine that means QJSEngine is actively developed. So, I guess I could just add a QJSEngine to my app and get it to work that way.
You wouldn't be able to talk to your QtQuick scene so I see no benefits of that.


I must be blind... I rechecked every first sentence from every paragraph you wrote. I find no further info on the asset system nor any link to it. The only link you provided is to QScriptEngine.
"Come back tomorrow" :)


There is also an issue with the example you provided. It seams less intuitive to adjust the build function.

In my initial solution this would work.

function build()
{
function reply(text)
{
var t = parse(text);
updateGUI(t);
}

var m = new NetworkManager();
m.fetch("http://www.somePage.com");
}
I could change the function to this and add more info without having to adjust the app and recompile:

function build()
{
function reply(text)
{
var t = parse(text);

function innerReply(text)
{
var tInner = t + parseAdditionalInfoFromOtherWebsite(text);
updateGUI(tInner);
}

var m2 = new NetworkManager();
m2.fetch("http://www.someOtherPage.com");
}

var m = new NetworkManager();
m.fetch("http://www.somePage.com");
}
The same can be done with my approach.

motaito
16th April 2015, 12:16
It is behaving exactly like JavaScript because it is JavaScript. What you call "difference" is probably that your code is executed in different contexts as QML probably sandboxes its environment to prevent breaking it by an irresponsible script.Fair enough. But how could I then load a JavaScript library that contains more than one function or call a function in one file that uses a function defined in another file?

Say I have an extended JavaScript library to manipulate/parse strings in various ways. I can not load it with QQmlApplicationEngine::evaluate(). I have to reimplement the library by creating a qml component as a wrapper for the JavaScript library. Is that correct?

I am asking because that would make QQmlApplicationEngine::evaluate() practically useless. It could not be used for anything more than a trivial function (one single function). And even then you would better write the function inside a qml component. So, it would be better not to have QQmlApplicationEngine::evaluate() at all. Thereby avoiding all the confusion and force a user to work with qml components from the start which would be the new way to do things anyway.

I'll rewrite my app to use qml components when I get back from work and let you know how it works for me.

wysota
16th April 2015, 12:56
Fair enough. But how could I then load a JavaScript library that contains more than one function or call a function in one file that uses a function defined in another file?

Say I have an extended JavaScript library to manipulate/parse strings in various ways. I can not load it with QQmlApplicationEngine::evaluate().
Well, from purely architectonic point of view you shouldn't want to do that. What you are trying to do thoughout this thread is IMO the exact opposite of what I'd call "scripting". When you script something, you expose some environment for the script to use in a controlled fashion, that is the script has access to nothing more than you allow it to. In your case you want the script to be fully in control of the environment (e.g. be allowed to delete or replace objects it doesn't own).

If you create a library that manipulates data in a certain way and you want to "register it" in the environment then something in the environment should know how to use it thus all names and algorithms from the script would have to be known in the main environment. In that case you could use the import statement to import the document. Hard to call that scripting.


I have to reimplement the library by creating a qml component as a wrapper for the JavaScript library. Is that correct?
No. You create a QML component if you want to use a QML component. It gives you much richer API than a number of dangling functions. And it is definitely more object oriented. I'd say that if you were to do this in pure javascript, you should also expose an object implementing a well-known API instead of providing dangling functions.


I am asking because that would make QQmlApplicationEngine::evaluate() practically useless. It could not be used for anything more than a trivial function (one single function). And even then you would better write the function inside a qml component. So, it would be better not to have QQmlApplicationEngine::evaluate() at all. Thereby avoiding all the confusion and force a user to work with qml components from the start which would be the new way to do things anyway.
To be honest I would not want some arbitrary code to interfere with my Qt Quick app. I would prefer to have complete control over what can be done with my environment.

To have some control over what is going on, you could pass a context object to your hook and scripts could modify that object however they wanted. That would limit the scope of unsanitized actions to that object only.

// contextObject.label = some instance of Text in QML scripts are allowed to manipulate

function init(contextObject) {
contextObject.myFunc = function() { ... }
}

function someOtherHook(contextObject) {
contextObject.myFunc()
contextObject.label.text = "Hello"
contextObject.label.color = "red"
}

motaito
16th April 2015, 15:08
Well, from purely architectonic point of view you shouldn't want to do that. What you are trying to do thoughout this thread is IMO the exact opposite of what I'd call "scripting". When you script something, you expose some environment for the script to use in a controlled fashion, that is the script has access to nothing more than you allow it to. In your case you want the script to be fully in control of the environment (e.g. be allowed to delete or replace objects it doesn't own).

That is not what I want to do at all. suppose I would want to do something like this:

1. go to "http://www.episodeworld.com/show/Big_Bang_Theory/season=8/english"
2. grab the title of the next episode that comes up
3. grap the imdb link
4. go to "http://www.imdb.com/title/tt0898266/"
5. grab the imdb rating for the show
6. form a string like:
"The Big Bang Theory (rating 8.5)
coming up: The Communication Deterioration (2015-04-16)"
7. show the result in the GUI

or

1. go to "http://www.imdb.com/movies-coming-soon/"
2. grap the title of the first 5 upcomming movies
3. form a string like:
"The Age of Adaline (2015)
Little Boy (2015)
The Water Diviner (2014)
Adult Beginners (2014)
Misery Loves Comedy (2015)"
7. show the result in the GUI

or

1. to to "http://groundation.com/tour-date/"
2. grab the next tour date
3. form a string like:
"17/APRA Miracle Tour 2015Nantes, France // La Trocardiere"
7. show the result in the GUI

From the app side that could all have the same logic (Get info online and present it).

I define what I have available (extendable in script or config file)

// in external JavaScript (buildDef.js)
var buildDef = [
{
identifier: "nextEpisode",
name: "The Big Bang Theory",
url: ""http://www.episodeworld.com/show/Big_Bang_Theory/season=8/english""
buildFunction: episodeBuildFunctionCallback
},
{
identifier: "upcommingMovies",
name: "",
url: "http://www.imdb.com/movies-coming-soon/"
buildFunction: movieBuildFunctionCallback
},
{
identifier: "groundationTour",
name: "",
url: "http://groundation.com/tour-date/",
buildFunction: tourBuildFunctionCallback
}
];

In another script I could add build functions as needed.


// in external JavaScript (buildFunc.js)
// e.g. in the first case:
// helper
function parseEpList(replyFromNetworkManager)
{
// parse replyFromNetworkManager
return "The Big Bang Theory {0} coming up: The Communication Deterioration (2015-04-16)";
}
// helper
function parseEpListToImdbUrl(replyFromNetworkManager)
{
// parse replyFromNetworkManager
return "http://www.imdb.com/title/tt0898266/";
}
// helper
function parseIMDB(replyFromNetworkManager)
{
// parse replyFromNetworkManager
return "(rating 8.5)";
}
// actual build function callback
function episodeBuildFunctionCallback(index)
{
function reply(text)
{
var epPart = parseEpList(text)

function innerReply(text)
{
var imdbPart = parseIMDB(text);
UpdateTextInQml(epPart.replace("{0}", imdbPart));
}

var url = parseEpListToImdbUrl(text);
var m2 = new NetworkManager();
m2.fetch(url);
}

var m = new NetworkManager();
m.fetch(buildDef[index].url);
}
// add similar functions for the remaining buildDef's.

Then I only need a build(identifier, name) function or functionality in the qml with the job to do this:
- find the appropriate build function based on identifier and name
- run the build function (e.g. episodeBuildFunctionCallback)


// in qml
...
MouseArea {
...
onClicked: {
// find index for buildDefinition based on identifier, dropdown selection or whatever and initiate the build
buildDef[index].buildFunction(index);
}
}

Text {
id: myText
text: "This will be updated."
}
...


So, I don't expose all of the environment to the script. Only one Text component needs to be exposed as well as the network manager. It seams to me by loading external qml files, I expose potentially more of the environment.

The script should be able to create an instance of a network manager and update one single Text component. In case the script fails the Text components is empty. Nothing else in the environment gets touched. Yet I can extend/adjust the "buildDef.js" and "buildFunc.js" as needed if I want to add info that can be found online. I would also add some sort of dropdown that gets filled based on the buildDef's so I can select what Info I want to find.

The app has one task. Grab info online and present it. The script defines how that happens. Is that not scripting? Do I have the terminology wrong?

Maybe I use the wrong terminology. But to me that is scripting. If I want to add some other info I add to the buildDef's and add a new buildFunction without having to touch or recompile the app at all. But there will be more than one function in "buildFunc.js" and they will call each other. So, QQmlApplicationEngine::evaluate() can not handle that. Correct?

I have to find a solution with loading qml components as you pointed out in order to get a clean approach in achieving that. I will try to implement your suggestion later today. And let you know how it goes and whether it solves what I am looking for.


If you create a library that manipulates data in a certain way and you want to "register it" in the environment then something in the environment should know how to use it thus all names and algorithms from the script would have to be known in the main environment. In that case you could use the import statement to import the document. Hard to call that scripting.That sound like what a dll should do. That is certainly not what I am looking for.

wysota
16th April 2015, 15:43
That is not what I want to do at all. suppose I would want to do something like this:
(snip)

This looks like "plugins" and not "scripts" to me. The "plugin" exposes functionality and the main application calls it.


So, I don't expose all of the environment to the script. Only one Text component needs to be exposed as well as the network manager. It seams to me by loading external qml files, I expose potentially more of the environment.
You complain that the functions you define in the script are not visible from within QML context. If they were your script could call any function on any object defined in QML, effectively taking control over your program.


Is that not scripting?
In my opinion -- hardly. And I still think your extension should define a function which should be called by your main application and for that the extension doesn't need access to the scope of the application and the application doesn't need access to the scope of the function (apart the entry point to the function itself). The environment after calling the function should be identical to before the function was called, no symbols should be left over.


So, QQmlApplicationEngine::evaluate() can not handle that. Correct?
Evaluate is not a virtual method. Therefor QJSEngine::evaluate() works exactly the same as QQmlApplicationEngine::evaluate().


That sound like what a dll should do. That is certainly not what I am looking for.
DLL = "Dynamically loaded library". Hence your "script" is also "DLL". Correct me if I'm wrong but from all your code snippets it looks like the "script" should implement a function with a well defined name and parameters that will be called from the main application (e.g. line #7 of your last snippet). That's exactly the use case I am talking about.

Furthermore you struggle to obtain a declarative environment using imperative language (e.g. your buildDef.js) while having an existing declarative environment at hand. If you leverage the features of QML you will get an environment where the application will not care whether you implement your "builder" in javascript, C++ or any other language as long as it has a QML binding.

motaito
16th April 2015, 17:23
Awesome, your post just cleared up a lot of my confusion. Thanks.


This looks like "plugins" and not "scripts" to me. The "plugin" exposes functionality and the main application calls it.Interesting! Maybe I should have looked into building a plugin system instead. I have never even given it any thoughts nor looked at how something like this would be done. The one part that I am unsure about is that I would have a network manager in the application providing functionality. That would then be used in a plugin to get functionality back into the application... Maybe that's why I got stuck on scripts instead of thinking about plugins. I will have to do some reading on the topic. It shouldn't be too hard to build a small plugin system, right?


In my opinion -- hardly. And I still think your extension should define a function which should be called by your main application and for that the extension doesn't need access to the scope of the application and the application doesn't need access to the scope of the function (apart the entry point to the function itself). The environment after calling the function should be identical to before the function was called, no symbols should be left over.I think that cleared up for me where I misunderstood what scripting should be. Or at least why my approach is not really considered scripting.


Evaluate is not a virtual method. Therefor QJSEngine::evaluate() works exactly the same as QQmlApplicationEngine::evaluate().Hmm, I still can not figure out how it can be the same. QQmlApplicationEngine::evaluate() requires parenthesis around the script, QJSEngine::evaluate() does not. I can not have more than one function in QQmlApplicationEngine::evaluate(). Even if they are in a sand-boxed environment or a different context. If they are built the same way I should be able to have more than one function in both cases and later call a function by name... It seams QQmlApplicationEngine::evaluate() must evaluate an expression while QJSEngine::evaluate() loads parts/functions for individual use. Be that as it may, I will not use this approach anyway. I was just hoping to learn how to use it out of interest. I will move on from this and put it behind me.


DLL = "Dynamically loaded library". Hence your "script" is also "DLL". Correct me if I'm wrong but from all your code snippets it looks like the "script" should implement a function with a well defined name and parameters that will be called from the main application (e.g. line #7 of your last snippet). That's exactly the use case I am talking about.Ok, I get now how you look at it. From that perspective yes it's actually pretty much like a dll. That also clears for me why earlier you pointed out that what I am doing is hardly scripting.


If you leverage the features of QML you will get an environment where the application will not care whether you implement your "builder" in JavaScript, C++ or any other language as long as it has a QML binding.I need to get a better grasp of what it means to work with a declarative environment. I am still driving my Ferrari in the parking lot :)

I will work a bit with the various ways you suggested to make my app more declarative and give some feedback later whether my issue is solved.

motaito
16th April 2015, 21:42
I try to implement your suggestion but I keep having issues loading the qml.


Component {
id: networkManager
url: "../network/NetworkManagerQml.qml"
}

I get this error:
Component elements may not contain properties other than id

I have also tried this:

...
MouseArea {
id: maRename
anchors.fill: parent
onClicked: {
var component = Qt.createComponent("../network/NetworkManagerQml.qml");
if (component.status == Component.Ready) {
var obj = component.createObject(myText);
if (null == obj)
console.log("Error creating component: " + component.errorString());
else
{
obj.ready.connect(function(text){ myText.text = text; obj.destroy(); });
obj.fetch("http://www.google.com");
}
}
else
console.log("Error loading component " + component.errorString());
}
}
I get this error:
Error loading component qrc:/component/network/NetworkManagerQml.qml: 13 Cannot assign to non-existaent default property.

If it helps my class looks like this:

#ifndef NETWORKMANAGER_H
#define NETWORKMANAGER_H

#include <QObject>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>


class NetworkManager : public QObject
{
Q_OBJECT

public:
NetworkManager(QObject *parent=0);
Q_INVOKABLE void fetch(const QString& url);

public slots:
void replyFinished(QNetworkReply*);

signals:
void reply(const QString& reply);

private:
QNetworkAccessManager* m_NetworkManager;
};

#endif // NETWORKMANAGER_H


my NetworkManagerQml.qml

import QtQuick 2.4
import MoeNetworkManager 1.0

QtObject {
id: manager
signal ready(string text)

function fetch(url) {
networkManager.fetch(url);
}

NetworkManagerHandler {
id: networkManager

onReply: {
manager.ready(text);
}
}
}

in my main.cpp

int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);

qmlRegisterType<NetworkManager>("MoeNetworkManager", 1, 0, "NetworkManagerHandler");

QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/component/main.qml")));

return app.exec();
}
Any idea whats going wrong?

Added after 1 43 minutes:

Sorry about the stupid question... Sometimes I don't get what I read. I have some dyslexia. In your example QtObject was meant to be a general component. I thought it actually has to be of the type QtObject. If I change it to e.g. Item it all works as expected. I am back on track now doing some more testing :)

The second snippet from above actually works. However, this

Component {
id: networkManager
url: "../network/NetworkManagerQml.qml"
}
doesn't work as expected. See error above. But that's ok I can load the object in a function.

wysota
16th April 2015, 23:11
The second snippet from above actually works. However, this

Component {
id: networkManager
url: "../network/NetworkManagerQml.qml"
}
doesn't work as expected.

Yeah, it's incorrect. You'd need to use Loader instead of that. Component defines a component inline, not refers to an external component. Which is actually pretty stupid that there is no element to load an external component like that :) I guess you could declare a Component with a Loader inside but then your actual component would have to inherit Item.

As for your question regarding QtObject -- yes, it was a general idea. I'd probably create a dedicated element for this purpose so that I'm sure all implementations have correct API.

anda_skoa
17th April 2015, 08:16
The second snippet from above actually works. However, this

Component {
id: networkManager
url: "../network/NetworkManagerQml.qml"
}
doesn't work as expected. See error above. But that's ok I can load the object in a function.



Component {
NetworkManagerQml {
}
}


Cheers,
_

wysota
17th April 2015, 09:02
Component {
NetworkManagerQml {
}
}

The point is to be able to dynamically choose the component so this will of course work but will not do the job of solving the original problem.

motaito
17th April 2015, 10:05
@wysota
I have been playing with this for a while and made some test. So far it's a much better solution than what I had initially. I am very grateful that you took the time to explain everything and provided all the help!


Component defines a component inline, not refers to an external component. Which is actually pretty stupid that there is no element to load an external component like thatI was also thinking that you should be able to just have a path to an external component and load it that way. Then again there is

import "some.qml" as ComponentName
which provides that functionality. The problem with this is that you have to provide an absolute path for components that are not in the qrc: resources. Since an import can not have a relative or dynamically build path there really should be an option to directly load/link a component based on an url. It would make some things easier.


@anda_skoa
That would essentially hard code it to one specific component. I need to dynamically load different qml files depending on the desired task. The path "../network/NetworkManagerQml.qml" was only there to point out it doesn't work. Technically it would be more like this:


Component {
id: networkManager
url: resolvePath(someTask);
}

function resolvePath(someTask) {
// get path to some qml file. Where the qml file
// can be different depending on the desired task
}

But that leads to the same problem. I neglected to go into more details because I thought that would be clear based on the ongoing discussion. I should have been more specific.

So far this


function loadRresource(requiredTask)
{
// get path to qml file depending on the task
var path = resolvePath(requiredTask);

var component = Qt.createComponent(path);
if (component.status == Component.Ready) {
var obj = component.createObject(myText);
if (null == obj)
console.log("Error creating component: " + component.errorString());
else
{
// this is the tricky part... The obj might need some sort of setup to work
// find a common way to initialize various components. e.g. setting up signals and slots
obj.someSetupFunctionHook();
}
}
else
console.log("Error loading component " + component.errorString());
}
seams to work best.


p.s. I will make sure I got it right before I mark the thread as solved. But so far I'm pretty happy with the result.

wysota
17th April 2015, 10:34
@wysota
I have been playing with this for a while and made some test. So far it's a much better solution than what I had initially. I am very grateful that you took the time to explain everything and provided all the help!

I was also thinking that you should be able to just have a path to an external component and load it that way. Then again there is

import "some.qml" as ComponentName
which provides that functionality. The problem with this is that you have to provide an absolute path for components that are not in the qrc: resources. Since an import can not have a relative or dynamically build path there really should be an option to directly load/link a component based on an url. It would make some things easier.

//ExternalComponent.qml
import QtQuick 2.0

QtObject {
id: root
readonly property real progress: 0
readonly property int status: Component.Null
property string url

function createObject(parent, properties) {
if(__cmp === null) return undefined
return __cmp.createObject(parent, properties)
}
function errorString() {
if(__cmp === null) return ""
return __cmp.errorString()
}

property bool _initialized: false
property var __cmp: null

onUrlChanged: __maybeInitComponent()

Component.onCompleted: {
__initialized = true
__maybeInitComponent()
}
function __maybeInitComponent() {
if(!__initialized) return
if(url === "") { __cmp = null; return; }
__cmp = Qt.createComponent(url)
__cmp.statusChanged.connect(function(st) { root.status = st; })
__cmp.progressChanged.connect(function(pr) { root.progress = pr; })
status = __cmp.status
progress = __cmp.progress
}
}

Not tested :)

anda_skoa
17th April 2015, 11:10
@anda_skoa
That would essentially hard code it to one specific component. I need to dynamically load different qml files depending on the desired task. The path "../network/NetworkManagerQml.qml" was only there to point out it doesn't work.

Then just use the "source" property of the Loader instead of the "sourceComponent" property.
The latter is for declaring components using Component, the former is for your use case, i.e. specifying the element using an URL.



Technically it would be more like this:


Component {
id: networkManager
url: resolvePath(someTask);
}




Loader {
source: resolvePath(someTask);
}


Cheers,
_

motaito
17th April 2015, 13:54
@wysota
If it doesn't exist, you can always build it yourself :) While that's an interesting approach, does it actually make it easier than loading the component in a JavaScript function?

@anda_skoa
A Loader might be working for me. Right now I have my tasks in a list. If I click on an item, the component is created. It then makes it's task and deletes itself again. Would that also work when I just clear the source from the Loader (source: "") after the task is completed? I'll try it later when I'm back at at home. Also, I'm unsure this is ideal form me because I need to hook up some signals. What if they are not always the same for each qml file that gets loaded. As far as I know the loader requires something like this:

Item {
Loader {
id: myLoader
source: "MyItem.qml"
}

Connections {
target: myLoader.item
onSignal: slot(arg) // these need to be flexible
}
}
in a JavaScript I can adjust the object creation based on the task. Yet I still have the option to load just one qml file. With a Loader I might need to do something like:

Item {
Loader {
id: loader0
source: ""
}

Connections {
target: loader0.item
onSignal: slot(arg)
}

Loader {
id: loader1
source: ""
}

Connections {
target: loader1.item
onOtherSignal: otherSlot(arg1, arg2)
}

// add loader placeholders as needed

function performTask(task)
{
// clear loaders, not sure that actually works...
loader0.source = "";
loader1.source = "";

// load necessary qml
if(1 == task)
loader0.source = "some.qml"
else
loader1.source = "other.qml"
}
}
Can a loader be cleared like that? Does then the unneeded Loader take up memory? Technically it also is some object instance so it probably should, even if just a little, right?

wysota
17th April 2015, 13:59
Note: While Loader itself is an Item, it can actually load objects which are not items themselves (at least that's what the docs claim).

motaito
17th April 2015, 14:51
And I just found the answer to my previous question in the docs as well

If the source or sourceComponent changes, any previously instantiated items are destroyed. Setting source to an empty string or setting sourceComponent to undefined destroys the currently loaded object, freeing resources and leaving the Loader empty.I guess I'll just try it and see which works best for me.

anda_skoa
17th April 2015, 15:26
Would that also work when I just clear the source from the Loader (source: "") after the task is completed?

Yes. Also when "active" is set to false.



As far as I know the loader requires something like this:

Item {
Loader {
id: myLoader
source: "MyItem.qml"
}

Connections {
target: myLoader.item
onSignal: slot(arg) // these need to be flexible
}
}

I am not sure what you mean with "this" in "requires this".
You mean usage of a Connections element?
If so instead of what? Calling connect on the signal and passing the slot?

Cheers,
_

motaito
17th April 2015, 16:12
I am not sure what you mean with "this" in "requires this".
You mean usage of a Connections element?
If so instead of what? Calling connect on the signal and passing the slot?I meant in order to hook up a signal-slot connection that a qml file might need. I am just wondering if I could load different qml files where they have different signals that they emit. In a JavaScript I can connect the signals to slots as needed. But I'm not sure how to do that with a Loader, because it seams that the connection has to be done in the "Connections {}" element. I don't think I can define one single Loader and have variable amounts of signal-slot connections depending on the qml file that gets loaded. Or is that possible?

E.g. some qml file emits signals to update a progress bar, when the task is done it sends a signal to update a text. Another qml might not need the progress bar and only sends a signal to update a text.

Would I have to pre-define the slots in the "Connections {}"? How would I treat such a slot if a loaded qml file wont need it?

Connections {
onProgress: updateProgressBar(value) // how to treat this if it's not needed by one of the qml files?
onText: showText(text)
}Can I just ignore it because the file would never emit a signal, or would I have to somehow clear the connection?

I am probably over thinking the issue... But you made me curious. I like to tidy up everything. Having unneeded parts leaves me restless :) I probably have to look at it more like an interface that allows for certain options even if one of the loaded qml files won't need all of them. But will the Loader take care of it on it's own?

wysota
17th April 2015, 17:11
I meant in order to hook up a signal-slot connection that a qml file might need. I am just wondering if I could load different qml files where they have different signals that they emit. In a JavaScript I can connect the signals to slots as needed. But I'm not sure how to do that with a Loader, because it seams that the connection has to be done in the "Connections {}" element. I don't think I can define one single Loader and have variable amounts of signal-slot connections depending on the qml file that gets loaded. Or is that possible?
Since the element being loaded knows what signals it requires (or provides) it is the one who should be responsible for making those connections.

motaito
17th April 2015, 18:49
Meanwhile I tested it. The loader works pretty great.

// foo.qml
Item {
id: root
signal foo(string txt)
anchors.fill: parent

MouseArea {
anchors.fill: parent
onClicked: {
root.foo("foo clicked");
}
}
}

// bar.qml
Item {
id: root
signal bar(string txt)
signal baz(string txt)
anchors.fill: parent

MouseArea {
anchors.fill: parent
onClicked: {
root.bar("bar clicked");
root.baz("baz clicked");
}
}
}

// a qml with a loader
Loader {
id: loader
source: ""
}
Connections {
onFoo: textFoo.text = txt // some text component
onBar: textBar.text = txt // some text component
onBaz: textBaz.text = txt // some text component
}

// execute on some mouse click
function build()
{
if("foo.qml" == loader.source)
loader.source = "bar.qml"
else
loader.source = "foo.qml"
}
It behaves as expected without having to set up anything further. No manual hooking of signal to slots is required. However, if foo.qml gets set as source I have a debug output like:

QML Connections: Cannot assign to non-existent property "onBar"
QML Connections: Cannot assign to non-existent property "onBaz"
and likewise if I set bar.qml as source I have:

QML Connections: Cannot assign to non-existent property "onFoo"
I don't know if I can savely ignore these outputs as they are expected. I am not sure that's good. What do you think, should I just ignore the warrnings and use a Loader?

wysota
17th April 2015, 18:53
Your Connections element lacks a target. There is also ignoreUnknownSignals property that will prevent such warnings.

motaito
17th April 2015, 19:28
There is also ignoreUnknownSignals property that will prevent such warnings.
Dame, it is so much easier to find stuff in the docu if you know what to look for.

ignoreUnknownSignals : bool
Normally, a connection to a non-existent signal produces runtime errors.

If this property is set to true, such errors are ignored. This is useful if you intend to connect to different types of objects, handling a different set of signals for each object.So, it's a fairly common thing with a solution ready and not really an issue. Exactly what I was missing.

Thanks to all the help and tips in this thread I have significantly reduced and cleaned up my code. Furthermore, I improved on how to use and understand qml as a declarative environment. I very much appreciate the help from everyone! I think I mark the thread as solved now. I have everything I was looking for and more. The help form everybody is very much appreciated. A special Thanks to wysota for all the details, explanations an code snippets. Thanks again!

Edit: I will mark the thread as solved, as soon as I find out how :rolleyes:

wysota
18th April 2015, 00:56
Edit: I will mark the thread as solved, as soon as I find out how :rolleyes:

Eeem... there is no way to do that currently :)

anda_skoa
18th April 2015, 12:34
I meant in order to hook up a signal-slot connection that a qml file might need. I am just wondering if I could load different qml files where they have different signals that they emit. In a JavaScript I can connect the signals to slots as needed. But I'm not sure how to do that with a Loader, because it seams that the connection has to be done in the "Connections {}" element.

I know this is already solved, but remember that the Loader also has signals and a signal handler is a JavaScript context.
So even if Connections would not have been able to do what you need, you could still have done your connections in the Loader's onLoaded handler.

Cheers,
_