QML for JavaScript programmers: Difference between revisions
No edit summary |
No edit summary |
||
Line 1: | Line 1: | ||
[[Category:Developing_with_Qt::Qt Quick]]<br />[[Category:HowTo]]<br />[toc align_right="yes&quot; depth="3&quot;] | |||
= QML for a JavaScript programmer = | |||
The | 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 == | |||
# UI description in | 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 "public api&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 | |||
The main issue even with this composition is that | 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, | 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== | == 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 === | ||
<code>/*<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 "value&quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count > this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = &#39;object&#39; ? this[i] : &#123;value: this[i]&#125;); | |||
for (;i &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 "myapp.js&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 "value&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: "init&quot;, arg: top.list}); | |||
<br /> WorkerScript {<br /> id: werk<br /> source: "my2threadap.js&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: "click&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 "value&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 "value&quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count > this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = &#39;object&#39; ? this[i] : &#123;value: this[i]&#125;); | |||
for (;i &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 < 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 < 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 < 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&lt; webdata.count; i''+) {<br /> Code.processElement(webdata.get(i));<br /> }<br /> }<br /> }<br /> }<br />// ….<br />}<br /></code> | |||
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 === | |||
== | <code>import "app.js&quot; as Code<br />// ….<br /> QtObject {<br /> id: common<br /> property int n; // typeof a = &#39;number&#39; | ||
property string s; // typeof s = 'string'<br /> property ListElement list: ListElement {}<br /> Component.onCompleted: Code.init(common);<br /> }<br /></code> | |||
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. | |||
In fact, as JavaScript misses the capability to store | |||
Revision as of 14:45, 23 February 2015
[toc align_right="yes" depth="3"]
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" 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:
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>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.
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" 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>property variant a: [1, 2, 3]</code> 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>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.
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 "value&quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count > this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = &#39;object&#39; ? this[i] : &#123;value: this[i]&#125;);
for (;i &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 "myapp.js&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 "value&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: "init&quot;, arg: top.list});
<br /> WorkerScript {<br /> id: werk<br /> source: "my2threadap.js&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: "click&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 "value&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 "value&quot; for the ListModel if the array is flat<br />Array.prototype.flush = function() {<br /> var i;<br /> while (this.model.count > this.length) this.model.remove(this.model.count-1);<br /> for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = &#39;object&#39; ? this[i] : &#123;value: this[i]&#125;);
for (;i &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 < 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 < 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 < 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&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 "app.js&quot; as Code<br />// ….<br /> QtObject {<br /> id: common<br /> property int n; // typeof a = &#39;number&#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.