About
In this tutorial we will see how to create simple QtQuick applications and how they will communicate with OCaml. QtQuick code will be very short because this tutorial is about using OCaml and QtQuick together. To get more information about programming in QtQuick you can visit official help or QML book.
Demos described below and sources of this tutorial can be found in Github repository.
Changelog
Starting from version 0.5
lablqt
is known as lablqml
. The rename was proposed because we don’t create binding for Qt
itself, most of the library is strongly connected to QtQuick.
This tutorial is the second one about OCaml and QtQuick. You can get old one
here.
In lablqml
starting from version 0.3
external JSON file for specifying API is deprecated, PPX extension is required. That’s why, lablqml 0.3
requires OCaml 4.02 or newer.
"Hello world!" in QtQuick
In this chapter a very simple QtQuick application is described. It interacts with OCaml only one-way: by calling handler on mouse click.
In piece of code below application’s window is created, with colored area at the top and some text centered in it. Some properties of application’s window are set. Position of inner rectangle is described using anchors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import QtQuick 2.0
import QtQuick.Controls 1.0
ApplicationWindow {
title: "Hello world!"
color: "#FFFFDF"
width: 400
height: 450
Rectangle {
color: "lightgreen"
height: 150
anchors {
left: parent.left
right: parent.right
top: parent.top
}
Text {
text: "Click me!"
anchors.centerIn: parent
}
}
}
|
qml
extension. QtQuick is a collection of classes from which GUI can be built.This QML file is already a valid application. It can be executed without any compilation by command qmlscene Root.qml
. Running the application should look like as on this picture:
The normal way to interact between QtQuick and C++ is by exposing some objects into QtQuick engine. Lablqml
has specific API for exposing objects and allows to declare them in OCaml (instead on C++) by using PPX extension. Build system will compile all into final executable. Let’s describe class controller
in controller.ml
:
open QmlContext
class virtual controller = object(self)
method virtual onMouseClicked: unit -> unit[@@qtmeth]
end[@@qtclass]
The module QmlContext
can be found in ocamlfind package named lablqml
. This package and PPX extension named ppx_qt
are located in OPAM’s package lablqml
. To apply syntax extension on this file you need to execute the following command:
ocamlopt -I `ocamlfind query lablqml` -c -dsource -ppx "ppx_qt -destdir . -ext cpp" controller.ml
lablqml
has started to depend on the OCaml package ocaml-migrate-parsetree
. Compilation commands
will probably require small adjustments for lablqml
>= 0.6
.In controller.ml
you can see the class controller
with attribute qtclass
and its method onMouseClicked
with attribute qtmeth
. Without this attributes the class and its method will not be available in QtQuick.
After preprocessing the source file a virtual class called controller
will be generated. Let’s implement a virtual method onMouseClicked
in program.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | open QmlContext
(* Helper function to expose the object 'obj' into QtQuick engine.
* Argument 'name' means the name for this object in QtQuick
*
* It is not interesting function. You can simply copy and paste it.
*)
let expose ~name obj =
(* 'obj#handler' is raw C++ pointer to object. *)
set_context_property ~ctx:(get_view_exn ~name:"rootContext") ~name obj#handler
(* This function is called once before constructing window. It exposes some
* objects from OCaml to QtQuick engine
*)
let init: unit -> unit = fun () ->
let controller = object(self)
inherit Controller.controller (Controller.create_controller ()) as super
(* 'onMouseClicked' is the method marked with [@@qtmeth] attribute in 'controller.ml'
* We need to implement it. *)
method onMouseClicked () =
print_endline "OCaml says: Mouse Clicked!"
end in
(* Once object is created we make it available in QtQuick engine *)
expose ~name:"controller" controller
let () =
let qmlfile = "Root.qml" in
(* We start application and pass initialization function and main QML file *)
run_with_QQmlApplicationEngine Sys.argv init qmlfile
|
In the snippet of code above, the path to the QML file is relative. QML files can be embedded into final executable using Qt Resource System.
The QML file doesn’t know anything about the exposed object yet. So, let’s create some area which will receive mouse events and call OCaml if it is clicked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import QtQuick 2.0
import QtQuick.Controls 1.0
ApplicationWindow {
title: "Hello world!"
color: "#FFFFDF"
width: 400
height: 450
Rectangle {
color: "lightgreen"
height: 150
anchors {
left: parent.left
right: parent.right
top: parent.top
}
Text {
text: "Click me!"
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onClicked: {
// call OCaml there
controller.onMouseClicked()
}
}
}
}
|
Full code of first demo is available on github.
Qt object’s metamodel
Qt classes based on QObject
can have properties, signals, slots and methods invokable from QtQuick. All information about this stuff is generated from C++ header files during compilation phase.
For example, our method onMouseClicked
declared above is invokable, that’s why we can use it from QML. If it was not, we would get the following type error:
Property 'onMouseClicked' of object controller(0x153cd00) is not a function
Properties always have name, type and methods that return its values. They can also have methods that set values. When a property changes it can send a signal with the updated value.
Signals usually are connected with slots. See this C++ code, for example:
auto listener = new PortListener();
auto parser = new MessageParser();
connect(listener, SIGNAL(gotData(QByteArray)), parser, SLOT(onDataReceived(QByteArray)) );
As you can see the relation between objects is established independently from them, i.e. object’s classes are independent: we don’t need to think about passing callbacks or creating some heavy-weight design patterns. That’s the Qt way of avoiding dependency injection.
Adding properties
Let’s extend the first demo by counting how many times we have clicked on the window. We will do it by declaring a property of type int
. To do this we need to improve our class definition in controller.ml
:
open QmlContext
class virtual controller = object(self)
method virtual onMouseClicked: unit -> unit[@@qtmeth]
method virtual clicksCount: int[@@qtprop]
end[@@qtclass]
Let’s extend the object implementation by adding a click counter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | open QmlContext
let expose ~name obj =
set_context_property ~ctx:(get_view_exn ~name:"rootContext") ~name obj#handler
let init () =
let controller = object(self)
inherit Controller.controller (Controller.create_controller ()) as super
val mutable counter = 0
method getclicksCount () = counter
method onMouseClicked () =
print_endline "OCaml says: Mouse Clicked!";
counter <- counter+1;
self#emit_clicksCountChanged counter
end in
expose ~name:"controller" controller
let () =
run_with_QQmlApplicationEngine Sys.argv init "Root.qml"
|
When the user hits the mouse button the counter is incremented and a signal with the updated counter value is emitted by OCaml method emit_$(PROPERTY_NAME)Changed
. If Qt properties don’t notify GUI about these changes old values will be used and no changes will be applied.
How QtQuick will react to these changes will be defined in Root.qml
. The new rectangle with text inside will show how many times user have clicked. In this demo QtQuick layouts are used to place rectangles vertically. Anchors are not used to implement it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import QtQuick 2.0
import QtQuick.Controls 1.0
import QtQuick.Layouts 1.0
ApplicationWindow {
title: "OCaml&QtQuick demo 2"
property string backgroundColor: "#FFFFDF"
color: backgroundColor
width: 400
height: 450
ColumnLayout {
Rectangle {
color: "lightgreen"
Layout.preferredHeight: 150
Layout.preferredWidth: 400
Text {
text: "Click me!"
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onClicked: controller.onMouseClicked()
}
}
Rectangle {
color: "#ebf097"
Layout.preferredHeight: 150
Layout.preferredWidth: 400
Text{
anchors.centerIn: parent
text: "You have clicked " + controller.clicksCount + " times"
}
}
}
}
|
Our property is used in line
text: "You have clicked " + controller.clicksCount + " times"
At the right of the colon character should be a valid ECMAScript expression which is evaluated into a property value at runtime. It can be multi line, call some functions and methods and use many properties inside. When one of properties used inside changes the whole expression is reevaluated and the text
property gets updated with a new value.
Sources of this demo can be found on github. Final window should be similar to the one on this picture:
Adding signals
When a property is declared a signal is automatically created. Signals can be declared separately using the same PPX syntax extension.
1 2 3 4 5 6 7 8 | open QmlContext
class virtual controller = object(self)
method virtual hiGotten: message:string -> unit[@@qtsignal]
method virtual onMouseClicked: unit -> unit[@@qtmeth]
method virtual clicksCount: int[@@qtprop]
end[@@qtclass]
|
Now signal hiGotten
can be connected to a handler in QtQuick:
...
Connections {
target: controller
onHiGotten: console.log(message)
}
...
The on
prefix is a syntax convention to declare signal handlers (behind the scene they are slots which were mentioned above). Arguments of the signal (the message
in our example) are made available in the context by QtQuick engine. I.e. Qt infers these names from signal’s declaration. If a signal is declared in the OCaml side without labels Qt will not be able to get the signal argument names. That’s why signals should be declared using labeled arguments.
Syntax extension is expanded into method emit_$(SIGNAL_NAME)
which can be used in main OCaml file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | open QmlContext
let main () =
let controller_cppobj = Controller.create_controller () in
let controller = object(self)
inherit Controller.controller controller_cppobj as super
val mutable counter = 0
method getclicksCount () = counter
method onMouseClicked () =
counter <- counter+1;
self#emit_clicksCountChanged counter;
(* Send signal with a new message *)
self#emit_hiGotten (Printf.sprintf "Hi %d times" counter);
end in
set_context_property ~ctx:(get_view_exn ~name:"rootContext") ~name:"controller" controller#handler
let () =
run_with_QQmlApplicationEngine Sys.argv main "Root.qml"
|
Full code of this demo is available on github.
Supported types
QML has its own type system. It is not as strong or static as the OCaml one but basic types exist. When values are passed from C++ to QML they are wrapped into QVariant, which is a universal container (or universal type).
For simple types QML basic types suit well enough. But for more elaborated types (like structures with a number of fields inside) then the right approach is to create a QObject-derived class and put these field inside.
At the moment objects support in lablqml
is a little bit awkward but an application like QOcamlBrowser (screenshot below) can be created without this feature. If you have a better way to implement it, I would be glad to hear it.
The demo can be found there. In controller.ml
some methods and properties, which use the universal type Variant.t
, are declared.
open QmlContext
class virtual controller = object(self)
method virtual getobj: unit -> QVariant.t[@@qtmeth]
method virtual setobj: QVariant.t -> unit[@@qtmeth]
method virtual person: QVariant.t[@@qtprop]
end[@@qtclass]
We also need to create a class for the object that will be manipulated by these methods.
open QmlContext
class virtual item = object(self)
method virtual name: string[@@qtprop]
method virtual age : int[@@qtprop]
end[@@qtclass]
The methods and properties (declared with attributes) of the controller
will be used in the main QML file when the construction of root object is completed (in Component.completed
signal handler).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import QtQuick 2.0
import QtQuick.Controls 1.0
ApplicationWindow {
Component.onCompleted: {
// get object from OCaml as method's result
var o = controller.getobj();
console.log(o);
// use some methods
console.log(o.getname());
console.log(o.getage() );
// We pass three values to OCaml side
// N.B. method is the same
controller.setobj(o);
controller.setobj("asdf");
controller.setobj(15);
// get object from OCaml as property's value
// to get a property's value we don't need paretheses at the end
var p = controller.person;
console.log(p);
console.log(p.getname())
console.log(p.getage());
}
}
|
In the OCaml side, the value of universal variant is matched and a specific string is printed. The constructor `qobject
contains a value of type cppobj
which is a raw pointer to an instance of a C++ QObject
class. That’s why an OCaml object is constructed from this value in the first clause.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class myitem cppObj = object(self)
inherit Item.item cppObj as super
method getname () = "Vasya"
method getage () = 11
end
...
method setobj = function
| `qobject o ->
let item = new myitem o in
printf "qobject: %s %d\n%!" (item#getname ()) (item#getage())
| `string s -> printf "String: '%s'\n%!" s
| `int x -> printf "int %d\n%!" x
| `empty -> print_endline "empty"
...
|
To avoid pattern matching we can allow the use of a specific type in properties and methods which will represent a raw C++ pointer to QObject. It’s not obvious at the moment how good it will be because the current approach is more powerful but more verbose. If you have any ideas on this topic I will be glad to read e-mails from you.
At the moment not many types can be used in methods' signatures but any complex type can be implemented using variant
type. It should not be a problem to support QML basic types in syntax extension if needed.
Compiling applications using lablqml
OCaml+QtQuick applications can start up both from C++ and OCaml. Examples using makefiles are available in links above and at lablqml’s repository (This one seems to be full enough). Also QOCamlBrowser is available, it uses ocamlbuild as the build system (but is has some issues at the moment: we need to generate some code on ./configure
stage).
To compile sources manually we need to execute following commands. Let’s suppose that PPX syntax extension was used at files module1.ml
and module2.ml
. To compile them we need to execute:
ocamlopt -I `ocamlfind query lablqml` -c -ppx "ppx_qt -destdir . -ext cpp" module1.ml ocamlopt -I `ocamlfind query lablqml` -c -ppx "ppx_qt -destdir . -ext cpp" module2.ml
Let’s suppose that files module1.ml
and module2.ml
declare classes called class1
and class2
respectively. After executing of two commands above four files should be created: class1.h
, class1_c.cpp
, class2.h
and class2_c.cpp
. These C++ files should be compiled as they are:
g++ `pkg-config --cflags Qt5Quick` -fPIC -c class2_c.cpp -o class2_c.o g++ `pkg-config --cflags Qt5Quick` -fPIC -c class1_c.cpp -o class1_c.o
Header files contain meta-information about types. So, we need to apply Qt MetaObject compiler (a.k.a. moc
) and compile generated C++ files:
moc class1.h -o moc_class1.cpp g++ `pkg-config --cflags Qt5Quick` -fPIC -c moc_class1.cpp -o moc_class1.o moc class2.h -o moc_class2.cpp g++ `pkg-config --cflags Qt5Quick` -fPIC -c moc_class2.cpp -o moc_class2.o
Also we need main .ml
file which uses classes Module1.class1
and Module2.class2
:
ocamlfind opt -package lablqml -c program.ml
And now we can link everything into final executable:
ocamlfind opt -package lablqml -linkpkg \ -cclib -lstdc++ -ccopt -L`qmake -query QT_INSTALL_LIBS` \ -cclib -lQt5Quick -cclib -lQt5Qml -cclib -lQt5Network \ -cclib -lQt5Widgets -cclib -lQt5Gui -cclib -lQt5Core \ module1.cmx module1.cmx program.cmx \ class1_c.o class2_c.o moc_class1.o moc_class2.o \ -o a.out
Using lablqml
without code generation
The one may not be very happy about printing some C++ code to the source files and compiling them
separately. This approach may introduce compilations when the generated code is far from ideal
and requires adjustments. However, Qt follows the approach with compile-time code generation
and lablqml
supports it as described above.
Also, QtQuick has convenience class QQmlPropertyMap which
allows you to dynamically (without compile-time code generation) add new properties to the subclasses
of QQmlPropertyMap
. The support of this class contributed to lablqml
0.5
by
Orbital Fox.
Conclusion
There are many aspects of QtQuick which are not mentioned in this tutorial. For example, in the
previous tutorial you can read about classes for desktop applications which live in harmony with your platform’s style. There is Model-View framework available both in Qt and lablqml
which is used rather much in QOcamlBrowser. QtQuick has an embedded
ECMAScript engine in it and you can compile OCaml code using js_of_ocaml
and run it as a crossplatform application.
QtQuick renders itself with OpenGL, so particle simulations and shader effects are built in.
lablqml
was used in some industrial applications. Ask Orbital Fox for more detailed feedback.
P.S. Getting and installing Qt5
Using package repository
There is Qt 5.2.1 in Ubuntu Trusty and Qt5.3 in current Debian/testing (July 1, 2014). I have got Qt 5.3 using official installer but all code should work with Qt 5.2 too. If it doesn’t, please report me somehow.
Official packages
are available on Qt Project.
Building from Git
Official Qt Project wiki is
there. Below you
can see my-init
and configure
scripts which configure
Qt5 to build only modules needed by lablqml.
#!/usr/bin/env bash
MODULES=qtbase,qtdeclarative,qtjsbackend,qtactiveqt,qtquickcontrols
perl ./init-repository --module-subset=$MODULES -f $@
#!/bin/bin/env bash
set -x
./configure -developer-build -opensource -nomake examples -nomake tests -confirm-license \
-no-gtkstyle -no-glib -no-cups $@
Qt compiles some code during ./configure
stage, don’t worry, it’s
normal.
Compilation time depends on your machine. Some fellas have finished with -j8
in 5 minutes. On my Intel Core i3 I can do it with -j2
in 30 minutes.
I have discovered some issues with compilation my QOCamlBrowser app recently on Ubuntu 13.10 32bit on Intel Core 2 Duo CPU. My application was crashing in qt_memfill32_sse2 (dest=0x8910e10, value=0, count=194)
at painting/qdrawhelper_sse2.cpp:264
. So, I have decided to disable special instruction sets for Qt5 on this machine by improving ./my-configure
script. I put this stuff in P.S.
section because I haven’t tested it on my Intel i3 machine.
#!/usr/bin/env bash
set -x
NOFB="-no-directfb -no-linuxfb -no-eglfs -no-kms"
SQL="-no-sql-mysql -no-sql-sqlite"
NOSSE="-no-sse2 -no-sse3 -no-ssse3 -no-sse4.1 -no-sse4.2 -no-avx -no-avx2"
./configure -developer-build -opensource -nomake examples -nomake tests -confirm-license \
-no-gtkstyle -no-glib -no-cups -no-nis \
$SQL $NOFB $NOSSE \
$@
Value $NOFB
disables frame buffer and KMS support. I think these low-level interfaces are not needed for our OCaml GUI projects. Also we disable SQLite and MySQL DBMS in Qt. Value $NOSSE
disables all special instruction sets.With that ./my-configure
QtQuick examples in Qt still work and my QOcamlBrowser startups without crash.
If you want to compile Qt faster you can study next options of ./configure script
:
-no-feature-<feature> Do not compile in <feature>. -feature-<feature> .. Compile in <feature>. The available features are described in src/corelib/global/qfeatures.txt
There are some issues on Qt bugtracker about using pkg-config
on MacOS. It seems that they handle -F
option wrong. The solution is mentioned here.
If you should compile Qt from source you will be able to avoid this issue by passing special options to ./configure
script: -prefix /opt/Qt/5.2.1 -opensource -nomake tests -no-xcb -no-framework
.
Next script will be useful (as a part of ~/.bashrc
) for setting local environment for your Qt5 build:
with_qt5() {
export PATH=~/mand/prog/qt/qt5/qtbase/bin:$PATH # where Qt5 has been build
qmake -query QT_VERSION
export LD_LIBRARY_PATH=`qmake -query QT_INSTALL_LIBS`:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=`qmake -query QT_INSTALL_LIBS`/pkgconfig:$PKG_CONFIG_PATH
}