QML for JavaScript programmers: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
No edit summary
 
No edit summary
Line 1: Line 1:
=<span class="caps">QML</span> for a JavaScript programmer=
[[Category:Developing_with_Qt::Qt Quick]]<br />[[Category:HowTo]]<br />[toc align_right=&quot;yes&amp;quot; depth=&quot;3&amp;quot;]


Although <span class="caps">QML</span> is technically a JavaScript extension, there are few issues a JavaScript programmer needs to be aware before designing the application. The typical advice from the Qt folks is “<span class="caps">QML</span> is not meant for JavaScript but for C++ UI” or “JavaScript is slow and you should not use it” which is not very helpful. Although all those statements are honest and at least mostly true, rest assured that applications can be programmed without C++ as long as your user interface data structures are flat objects or single dimension arrays (lists) of objects. But there are few things to know first:
= QML for a JavaScript programmer =


The fundamental problem for the JavaScript programmer is that <span class="caps">QML</span> JavaScript is not really designed for JavaScript expressions but for Qt C++ objects. As the C++ and JavaScript data models are fundamentally different, the <span class="caps">QML</span> is designed to support only a limited set of the strongly typed QtObject derived objects and a superset of simple types over JavaScript types. Luckily, inside the JavaScript functions and namespace, you can use the full expressiveness of JavaScript but the interface between <span class="caps">QML</span> and JavaScript becomes C-like with named global variables declared in <span class="caps">QML</span> and passing the <span class="caps">QML</span> object references as arguments.
Although QML is technically a JavaScript extension, there are few issues a JavaScript programmer needs to be aware before designing the application. The typical advice from the Qt folks is &quot;QML is not meant for JavaScript but for C++ UI&amp;quot; or &quot;JavaScript is slow and you should not use it&amp;quot; which is not very helpful. Although all those statements are honest and at least mostly true, rest assured that applications can be programmed without C++ as long as your user interface data structures are flat objects or single dimension arrays (lists) of objects. But there are few things to know first:


On the other hand, <span class="caps">QML</span> implements a beautiful namespace model for JavaScript. Each JavaScript file is loaded to a defined namespace in the beginning of the <span class="caps">QML</span> file: <code>import ‘myapp.js’ as Code</code> and any variables defined in the top level of the file gets loaded to the object “Code”. Also implicitly any initialization code on the top level of the file will be executed on load.
The fundamental problem for the JavaScript programmer is that QML JavaScript is not really designed for JavaScript expressions but for Qt C++ objects. As the C++ and JavaScript data models are fundamentally different, the QML is designed to support only a limited set of the strongly typed QtObject derived objects and a superset of simple types over JavaScript types. Luckily, inside the JavaScript functions and namespace, you can use the full expressiveness of JavaScript but the interface between QML and JavaScript becomes C-like with named global variables declared in QML and passing the QML object references as arguments.


==Design pattern==
On the other hand, QML implements a beautiful namespace model for JavaScript. Each JavaScript file is loaded to a defined namespace in the beginning of the QML file: &lt;code&amp;gt;import 'myapp.js' as Code&amp;lt;/code&amp;gt; and any variables defined in the top level of the file gets loaded to the object &quot;Code&amp;quot;. Also implicitly any initialization code on the top level of the file will be executed on load.


The design pattern I have used is to split the application to
== Design pattern ==


# UI description in <span class="caps">QML</span> (''myapp.qml'')
The design pattern I have used is to split the application to<br /># UI description in QML (''myapp.qml'')<br /># Application functionality in JavaScript (''myapp.js'')<br /># The Common data for both (in a well-defined place in the ''myapp.qml'')<br /># Wrapper functions in QML file to create the &quot;public api&amp;quot; between JavaScript and QML passing the necessary QML object references (in the ''myapp.qml'').
# Application functionality in JavaScript (''myapp.js'')
# The Common data for both (in a well-defined place in the ''myapp.qml'')
# Wrapper functions in <span class="caps">QML</span> file to create the “public api” between JavaScript and <span class="caps">QML</span> passing the necessary <span class="caps">QML</span> object references (in the ''myapp.qml'').


The main issue even with this composition is that <span class="caps">QML</span> can not handle the JavaScript dynamic data structures. Even though one can declare a <code>property variant a: [1, 2, 3]</code> in <span class="caps">QML</span>, it is a readonly property. Any attempt to modify inside JavaScript silently (and miserably) fail. Do not waste your time attempting to hack yourself around it you won’t.
The main issue even with this composition is that QML can not handle the JavaScript dynamic data structures. Even though one can declare a &lt;code&amp;gt;property variant a: [1, 2, 3]&lt;/code&amp;gt; in QML, it is a readonly property. Any attempt to modify inside JavaScript silently (and miserably) fail. Do not waste your time attempting to hack yourself around it - you won't.


However, if you stick only to one-dimensional list and grid views in the UI, you’ll be fine. The way to go is to use the built-in ListModel data structure as interface to JavaScript. It behaves quite like a JavaScript Array but with the limitation that all members have to be simple compound objects with exactly the same set of members, i.e list members can not be lists. The pattern is to define a empty ListModel in the <span class="caps">QML</span> as <code>property ListModel myListModel: ListModel {}</code> and wrap it with a JavaScript Array with all updates to the ListModel only through the wrapper and never use the ListModel member functions <code>.get() .set() .clear() .append()</code> etc. directly.
However, if you stick only to one-dimensional list and grid views in the UI, you'll be fine. The way to go is to use the built-in ListModel data structure as interface to JavaScript. It behaves quite like a JavaScript Array but with the limitation that all members have to be simple compound objects with exactly the same set of members, i.e list members can not be lists. The pattern is to define a empty ListModel in the QML as &lt;code&amp;gt;property ListModel myListModel: ListModel {}&lt;/code&amp;gt; and wrap it with a JavaScript Array with all updates to the ListModel only through the wrapper and never use the ListModel member functions &lt;code&amp;gt;.get() .set() .clear() .append()&lt;/code&amp;gt; etc. directly.


==Code for a single threaded app==
== Code for a single threaded app ==


Here is my very simple boilerplate for a single threaded app:
Here is my very simple boilerplate for a single threaded app:


===myapp.js===
=== myapp.js ===


===MyApp.qml===
<code>/*<br /> * Array wrapper<br /> */


<span class="caps">QML</span> supports also the concept of a worker thread with nice integration to the ListModel, although the thread pool size seems to be only one in the current implementation. Worker code can not access the properties on the main thread and thus data needs to be passed through messages. ListModel is a special case and has a special .sync() method that can be conveniently integrated to the array wrapper.
// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array<br />Array.prototype.attach = function(listmodel) {<br /> this._''model = listmodel;<br /> this.flush();<br />}
<br />// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array<br />// automatically creates a role &quot;value&amp;quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count &gt; this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i &lt; this.model.count; i++) this.model.set(i, typeof this[i] = &amp;#39;object&amp;#39; ? this[i] : &amp;#123;value: this[i]&amp;#125;);
    for (;i &amp;lt; this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});<br />// this.model.sync(); // The model.sync() is only for updates in WorkerScript - see next example<br />}
<br />/*<br /> * Application<br /> */<br />var myArray = []; // This is the array to work with, you can initialize it here
<br />var init = function(listModel) { // QML calls when ready<br /> // Do the final initializations here
<br /> myArray.attach(listModel); // And finally attach the listModel to the array<br />}
<br />var arrayClickedAt = function(index) { // mouse click callback for the array item [i]<br /> // Do whatever magic needed to your array here..
<br /> myArray.flush(); // and finally update the ListModel<br />}<br /></code>
<br />h3. MyApp.qml
<br /><code>import Qt 4.7<br />import &quot;myapp.js&amp;quot; as Code
<br />Rectangle {<br /> id: top<br /> width: 360<br /> height: 360<br /> property ListModel list: ListModel {}<br /> Component.onCompleted: Code.init(top.list);
<br /> Component {<br /> id: delegate<br /> Text { // or whatever item<br /> MouseArea { // Item click handler<br /> anchors.fill: parent<br /> onClicked: Code.listClickedAt(index); // implement the click handler in the JavaScript<br /> }<br /> // Implement delegate with full access to the array element properties. Use the role &quot;value&amp;quot; if the array is flat<br /> text: value<br /> }<br /> }
<br /> ListView {<br /> anchors.fill: parent<br /> model: top.list;<br /> delegate: delegate;<br /> }<br />}<br /></code>
<br />QML supports also the concept of a worker thread with nice integration to the ListModel, although the thread pool size seems to be only one in the current implementation. Worker code can not access the properties on the main thread and thus data needs to be passed through messages. ListModel is a special case and has a special .sync() method that can be conveniently integrated to the array wrapper.
<br />h2. Code for an app with UI and logic in separate threads
<br />Here is the boilerplate for an app with UI and app logic in separate threads. Example takes long to populate 40 Fibonacci numbers to an array but shows the UI immediately.
<br />Compare this code to the previous.
<br />h3. My2ThreadApp.qml
<br /><code>import Qt 4.7
<br />Rectangle {<br /> id: top<br /> width: 400<br /> height: 800<br /> property ListModel list: ListModel {}<br /> Component.onCompleted: werk.sendMessage({msg: &quot;init&amp;quot;, arg: top.list});
<br /> WorkerScript {<br /> id: werk<br /> source: &quot;my2threadap.js&amp;quot;<br /> }
<br /> Component {<br /> id: delegate<br /> Text { // or whatever item<br /> MouseArea { // Item click handler<br /> anchors.fill: parent<br /> onClicked: werk.sendMessage({msg: &quot;click&amp;quot;, arg: index}); // implement the click handler in the JavaScript<br /> }<br /> // Implement delegate with full access to the array element properties. Use the role &quot;value&amp;quot; if the array is flat<br /> text: value<br /> }<br /> }
<br /> ListView {<br /> anchors.fill: parent<br /> model: top.list;<br /> delegate: delegate;<br /> }<br />}<br /></code><br />h3. my2threadapp.js
<br /><code>// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array<br />Array.prototype.attach = function(listmodel) {<br /> this.model = listmodel;<br /> this.flush();<br />}
<br />// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array<br />// automatically creates a role &quot;value&amp;quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count &gt; this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i &lt; this.model.count; i++) this.model.set(i, typeof this[i] = &amp;#39;object&amp;#39; ? this[i] : &amp;#123;value: this[i]&amp;#125;);
    for (;i &amp;lt; this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});<br /> this.''_model.sync(); // The model.sync() is for updates in WorkerScript<br />}


==Code for an app with UI and logic in separate threads==
function fibonacci(n) { // heavy computing<br /> if (n &lt; 3) {<br /> return 1;<br /> } else {<br /> return fibonacci(n-1)''fibonacci(n-2);<br /> }<br />}
<br />var myArray = []; // This is the array to work with
<br />var init = function(list) { // QML is ready<br /> var i, v;
<br /> myArray.attach(list); // Attach the top.list ListModel to the array
<br /> for (i = 0; i &lt; 40 ; i) { // fill the array, this loop takes a long time<br /> myArray[i] = fibonacci(i+1);<br /> myArray.flush(); // update the ListModel<br /> }<br />}
<br />var listClickedAt = function(index) { // mouse click callback for the array item [i]<br /> var half, i;
<br /> if (index === 0) return;<br /> half = myArray.splice(index, myArray.length-index);<br /> myArray.unshift(half.shift());<br /> for (i = 0; i &lt; half.length; i) myArray.push(half[i]);<br /> myArray.flush(); // update the ListModel<br />}
<br />var messages = {<br /> click: listClickedAt,<br /> init: init<br /> };
<br />WorkerScript&amp;lt; webdata.count; i''+) {<br /> Code.processElement(webdata.get(i));<br /> }<br /> }<br /> }<br /> }<br />// ….<br />}<br /></code>


Here is the boilerplate for an app with UI and app logic in separate threads. Example takes long to populate 40 Fibonacci numbers to an array but shows the UI immediately.
JavaScript is loaded to the same scope as the QML and thus can access any QML element by id. However, I find it most convenient to collect all properties that are shared between QML and JavaScript under one QtObject and pass it to the JavaScript side init() as argument to keep the object naming in control.


Compare this code to the previous.
=== app.qml ===


===My2ThreadApp.qml===
<code>import &quot;app.js&amp;quot; as Code<br />// ….<br /> QtObject {<br /> id: common<br /> property int n; // typeof a = &amp;#39;number&amp;#39;
  property string s; // typeof s = 'string'<br /> property ListElement list: ListElement {}<br /> Component.onCompleted: Code.init(common);<br /> }<br /></code>


h3. my2threadapp.js
In fact, as JavaScript misses the capability to store JSON to the device local storage, I eventually ended up extending this concept with implementing a Storage component that persistently stores all its properties across application executions and does the Array augmentation. And it is 100% JavaScript. Duh.
 
JavaScript is loaded to the same scope as the <span class="caps">QML</span> and thus can access any <span class="caps">QML</span> element by id. However, I find it most convenient to collect all properties that are shared between <span class="caps">QML</span> and JavaScript under one QtObject and pass it to the JavaScript side init() as argument to keep the object naming in control.
 
===app.qml===
 
In fact, as JavaScript misses the capability to store <span class="caps">JSON</span> to the device local storage, I eventually ended up extending this concept with implementing a Storage component that persistently stores all its properties across application executions and does the Array augmentation. And it is 100% JavaScript. Duh.
 
===Categories:===
 
* [[:Category:Developing with Qt|Developing_with_Qt]]
** [[:Category:Developing with Qt::Qt Quick|Qt_Quick]]
* [[:Category:HowTo|HowTo]]

Revision as of 14:45, 23 February 2015



[toc align_right="yes&quot; depth="3&quot;]

QML for a JavaScript programmer

Although QML is technically a JavaScript extension, there are few issues a JavaScript programmer needs to be aware before designing the application. The typical advice from the Qt folks is "QML is not meant for JavaScript but for C++ UI&quot; or "JavaScript is slow and you should not use it&quot; which is not very helpful. Although all those statements are honest and at least mostly true, rest assured that applications can be programmed without C++ as long as your user interface data structures are flat objects or single dimension arrays (lists) of objects. But there are few things to know first:

The fundamental problem for the JavaScript programmer is that QML JavaScript is not really designed for JavaScript expressions but for Qt C++ objects. As the C++ and JavaScript data models are fundamentally different, the QML is designed to support only a limited set of the strongly typed QtObject derived objects and a superset of simple types over JavaScript types. Luckily, inside the JavaScript functions and namespace, you can use the full expressiveness of JavaScript but the interface between QML and JavaScript becomes C-like with named global variables declared in QML and passing the QML object references as arguments.

On the other hand, QML implements a beautiful namespace model for JavaScript. Each JavaScript file is loaded to a defined namespace in the beginning of the QML file: <code&gt;import 'myapp.js' as Code&lt;/code&gt; and any variables defined in the top level of the file gets loaded to the object "Code&quot;. Also implicitly any initialization code on the top level of the file will be executed on load.

Design pattern

The design pattern I have used is to split the application to
# UI description in QML (myapp.qml)
# Application functionality in JavaScript (myapp.js)
# The Common data for both (in a well-defined place in the myapp.qml)
# Wrapper functions in QML file to create the "public api&quot; between JavaScript and QML passing the necessary QML object references (in the myapp.qml).

The main issue even with this composition is that QML can not handle the JavaScript dynamic data structures. Even though one can declare a <code&gt;property variant a: [1, 2, 3]</code&gt; in QML, it is a readonly property. Any attempt to modify inside JavaScript silently (and miserably) fail. Do not waste your time attempting to hack yourself around it - you won't.

However, if you stick only to one-dimensional list and grid views in the UI, you'll be fine. The way to go is to use the built-in ListModel data structure as interface to JavaScript. It behaves quite like a JavaScript Array but with the limitation that all members have to be simple compound objects with exactly the same set of members, i.e list members can not be lists. The pattern is to define a empty ListModel in the QML as <code&gt;property ListModel myListModel: ListModel {}</code&gt; and wrap it with a JavaScript Array with all updates to the ListModel only through the wrapper and never use the ListModel member functions <code&gt;.get() .set() .clear() .append()</code&gt; etc. directly.

Code for a single threaded app

Here is my very simple boilerplate for a single threaded app:

myapp.js

/*<br /> * Array wrapper<br /> */

// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array<br />Array.prototype.attach = function(listmodel) {<br /> this._''model = listmodel;<br /> this.flush();<br />}
<br />// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array<br />// automatically creates a role &quot;value&amp;quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count &gt; this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i &lt; this.model.count; i++) this.model.set(i, typeof this[i] = &amp;#39;object&amp;#39; ? this[i] : &amp;#123;value: this[i]&amp;#125;);
    for (;i &amp;lt; this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});<br />// this.model.sync(); // The model.sync() is only for updates in WorkerScript - see next example<br />}
<br />/*<br /> * Application<br /> */<br />var myArray = []; // This is the array to work with, you can initialize it here
<br />var init = function(listModel) { // QML calls when ready<br /> // Do the final initializations here
<br /> myArray.attach(listModel); // And finally attach the listModel to the array<br />}
<br />var arrayClickedAt = function(index) { // mouse click callback for the array item [i]<br /> // Do whatever magic needed to your array here..
<br /> myArray.flush(); // and finally update the ListModel<br />}<br />


h3. MyApp.qml


import Qt 4.7<br />import &quot;myapp.js&amp;quot; as Code
<br />Rectangle {<br /> id: top<br /> width: 360<br /> height: 360<br /> property ListModel list: ListModel {}<br /> Component.onCompleted: Code.init(top.list);
<br /> Component {<br /> id: delegate<br /> Text { // or whatever item<br /> MouseArea { // Item click handler<br /> anchors.fill: parent<br /> onClicked: Code.listClickedAt(index); // implement the click handler in the JavaScript<br /> }<br /> // Implement delegate with full access to the array element properties. Use the role &quot;value&amp;quot; if the array is flat<br /> text: value<br /> }<br /> }
<br /> ListView {<br /> anchors.fill: parent<br /> model: top.list;<br /> delegate: delegate;<br /> }<br />}<br />


QML supports also the concept of a worker thread with nice integration to the ListModel, although the thread pool size seems to be only one in the current implementation. Worker code can not access the properties on the main thread and thus data needs to be passed through messages. ListModel is a special case and has a special .sync() method that can be conveniently integrated to the array wrapper.
h2. Code for an app with UI and logic in separate threads
Here is the boilerplate for an app with UI and app logic in separate threads. Example takes long to populate 40 Fibonacci numbers to an array but shows the UI immediately.
Compare this code to the previous.
h3. My2ThreadApp.qml


import Qt 4.7
<br />Rectangle {<br /> id: top<br /> width: 400<br /> height: 800<br /> property ListModel list: ListModel {}<br /> Component.onCompleted: werk.sendMessage({msg: &quot;init&amp;quot;, arg: top.list});
<br /> WorkerScript {<br /> id: werk<br /> source: &quot;my2threadap.js&amp;quot;<br /> }
<br /> Component {<br /> id: delegate<br /> Text { // or whatever item<br /> MouseArea { // Item click handler<br /> anchors.fill: parent<br /> onClicked: werk.sendMessage({msg: &quot;click&amp;quot;, arg: index}); // implement the click handler in the JavaScript<br /> }<br /> // Implement delegate with full access to the array element properties. Use the role &quot;value&amp;quot; if the array is flat<br /> text: value<br /> }<br /> }
<br /> ListView {<br /> anchors.fill: parent<br /> model: top.list;<br /> delegate: delegate;<br /> }<br />}<br />


h3. my2threadapp.js

// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array<br />Array.prototype.attach = function(listmodel) {<br /> this.model = listmodel;<br /> this.flush();<br />}
<br />// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array<br />// automatically creates a role &quot;value&amp;quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count &gt; this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i &lt; this.model.count; i++) this.model.set(i, typeof this[i] = &amp;#39;object&amp;#39; ? this[i] : &amp;#123;value: this[i]&amp;#125;);
    for (;i &amp;lt; this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});<br /> this.''_model.sync(); // The model.sync() is for updates in WorkerScript<br />}

function fibonacci(n) { // heavy computing<br /> if (n &lt; 3) {<br /> return 1;<br /> } else {<br /> return fibonacci(n-1)''fibonacci(n-2);<br /> }<br />}
<br />var myArray = []; // This is the array to work with
<br />var init = function(list) { // QML is ready<br /> var i, v;
<br /> myArray.attach(list); // Attach the top.list ListModel to the array
<br /> for (i = 0; i &lt; 40 ; i) { // fill the array, this loop takes a long time<br /> myArray[i] = fibonacci(i+1);<br /> myArray.flush(); // update the ListModel<br /> }<br />}
<br />var listClickedAt = function(index) { // mouse click callback for the array item [i]<br /> var half, i;
<br /> if (index === 0) return;<br /> half = myArray.splice(index, myArray.length-index);<br /> myArray.unshift(half.shift());<br /> for (i = 0; i &lt; half.length; i) myArray.push(half[i]);<br /> myArray.flush(); // update the ListModel<br />}
<br />var messages = {<br /> click: listClickedAt,<br /> init: init<br /> };
<br />WorkerScript&amp;lt; webdata.count; i''+) {<br /> Code.processElement(webdata.get(i));<br /> }<br /> }<br /> }<br /> }<br />// ….<br />}<br />

JavaScript is loaded to the same scope as the QML and thus can access any QML element by id. However, I find it most convenient to collect all properties that are shared between QML and JavaScript under one QtObject and pass it to the JavaScript side init() as argument to keep the object naming in control.

app.qml

import &quot;app.js&amp;quot; as Code<br />// ….<br /> QtObject {<br /> id: common<br /> property int n; // typeof a = &amp;#39;number&amp;#39;
  property string s; // typeof s = 'string'<br /> property ListElement list: ListElement {}<br /> Component.onCompleted: Code.init(common);<br /> }<br />

In fact, as JavaScript misses the capability to store JSON to the device local storage, I eventually ended up extending this concept with implementing a Storage component that persistently stores all its properties across application executions and does the Array augmentation. And it is 100% JavaScript. Duh.