QML for JavaScript programmers

From Qt Wiki
Revision as of 20:18, 28 June 2015 by Wieland (talk | contribs) (Removed cleanup notice)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

En Ar Bg De El Es Fa Fi Fr Hi Hu It Ja Kn Ko Ms Nl Pl Pt Ru Sq Th Tr Uk Zh

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:

import 'myapp.js' as 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

  1. UI description in QML (myapp.qml)
  2. Application functionality in JavaScript (myapp.js)
  3. The Common data for both (in a well-defined place in the myapp.qml)
  4. 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

property variant a: [1, 2, 3]

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

property ListModel myListModel: ListModel {}

and wrap it with a JavaScript Array with all updates to the ListModel only through the wrapper and never use the ListModel member functions

.get() .set() .clear() .append()

etc. directly.

Code for a single threaded app

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

myapp.js

/*
 * Array wrapper
 */

// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array
Array.prototype.attach = function(listmodel) {
 this._model = listmodel;
 this.flush();
}

// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array
// automatically creates a role "value" for the ListModel if the array is flat
Array.prototype.flush = function() {
 var i;
 while (this.model.count > this.length) this.model.remove(this.model.count-1);
 for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = 'object' ? this[i] : {value: this[i]});
    for (;i < this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});
// this.model.sync(); // The model.sync() is only for updates in WorkerScript - see next example
}

/*
 * Application
 */
var myArray = []; // This is the array to work with, you can initialize it here

var init = function(listModel) { // QML calls when ready
 // Do the final initializations here

 myArray.attach(listModel); // And finally attach the listModel to the array
}

var arrayClickedAt = function(index) { // mouse click callback for the array item [i]
 // Do whatever magic needed to your array here..

 myArray.flush(); // and finally update the ListModel
}

MyApp.qml

import Qt 4.7
import "myapp.js" as Code

Rectangle {
 id: top
 width: 360
 height: 360
 property ListModel list: ListModel {}
 Component.onCompleted: Code.init(top.list);

 Component {
 id: delegate
 Text { // or whatever item
 MouseArea { // Item click handler
 anchors.fill: parent
 onClicked: Code.listClickedAt(index); // implement the click handler in the JavaScript
 }
 // Implement delegate with full access to the array element properties. Use the role "value" if the array is flat
 text: value
 }
 }

 ListView {
 anchors.fill: parent
 model: top.list;
 delegate: delegate;
 }
}

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.

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.

My2ThreadApp.qml

import Qt 4.7

Rectangle {
 id: top
 width: 400
 height: 800
 property ListModel list: ListModel {}
 Component.onCompleted: werk.sendMessage({msg: "init", arg: top.list});

 WorkerScript {
 id: werk
 source: "my2threadap.js"
 }

 Component {
 id: delegate
 Text { // or whatever item
 MouseArea { // Item click handler
 anchors.fill: parent
 onClicked: werk.sendMessage({msg: "click", arg: index}); // implement the click handler in the JavaScript
 }
 // Implement delegate with full access to the array element properties. Use the role "value" if the array is flat
 text: value
 }
 }

 ListView {
 anchors.fill: parent
 model: top.list;
 delegate: delegate;
 }
}

my2threadapp.js

// .attach() method to JavaScript Arrays: attaches a QML ListModel to an array
Array.prototype.attach = function(listmodel) {
 this.model = listmodel;
 this.flush();
}

// .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array
// automatically creates a role "value" for the ListModel if the array is flat
Array.prototype.flush = function() {
 var i;
 while (this.model.count > this.length) this.model.remove(this.model.count-1);
 for (i = 0; i < this.model.count; i++) this.model.set(i, typeof this[i] = 'object' ? this[i] : {value: this[i]});
    for (;i < this.length; i++) this.__model.append(typeof this[i] = 'object' ? this[i] : {value: this[i]});
 this._model.sync(); // The model.sync() is for updates in WorkerScript
}

function fibonacci(n) { // heavy computing
 if (n < 3) {
 return 1;
 } else {
 return fibonacci(n-1)+fibonacci(n-2);
 }
}

var myArray = []; // This is the array to work with

var init = function(list) { // QML is ready
 var i, v;

 myArray.attach(list); // Attach the top.list ListModel to the array

 for (i = 0; i < 40 ; i) { // fill the array, this loop takes a long time
 myArray[i] = fibonacci(i+1);
 myArray.flush(); // update the ListModel
 }
}

var listClickedAt = function(index) { // mouse click callback for the array item [i]
 var half, i;

 if (index === 0) return;
 half = myArray.splice(index, myArray.length-index);
 myArray.unshift(half.shift());
 for (i = 0; i < half.length; i) myArray.push(half[i]);
 myArray.flush(); // update the ListModel
}

var messages = {
 click: listClickedAt,
 init: init
 };

WorkerScript< webdata.count; i++) {
 Code.processElement(webdata.get(i));
 }
 }
 }
 }
// ...
}

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" as Code
// ...
 QtObject {
 id: common
 property int n; // typeof a = 'number';
  property string s; // typeof s = 'string'
 property ListElement list: ListElement {}
 Component.onCompleted: Code.init(common);
 }

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.