QML for JavaScript programmers

From Qt Wiki
Revision as of 16:02, 14 January 2015 by Maintenance script (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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:

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

MyApp.qml

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

h3. my2threadapp.js

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

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.

Categories: