C

Integrating C++ code with QML

Overview

The Qt Quick Ultralite applications use C++ code for business logic, interfacing with hardware, and working with other software modules. The C++ part of the application interacts with the QML presentation layer. Therefore the C++ classes as well as their functions, properties, and signals are exposed to QML code.

Interface headers

An application can contain interface headers that describe the components that shall be exposed to QML. These are normal C++ header files that are registered in CMake with the qul_target_generate_interfaces() macro.

When you register a header with the macro, it has two effects on the build:

  • the qmlinterfacegenerator tool will be run on the header, creating one or more QML interface files
  • the generated files will be fed to qmltocpp when translating the project's QML code

The steps enable QML code to make use of the C++ interfaces.

Exporting C++ APIs

Components

Class and struct declarations are the building blocks for exposing C++ functionality to QML. Each exported C++ class declaration defines a QML component of the same name.

The following example demonstrates how to use components:

#include <qul/object.h>
#include <qul/property.h>
#include <qul/signal.h>

struct BigState : public Qul::Object
{
    Qul::Property<int> bigness;
    int feed(int amount);
    Qul::Signal<void()> gotFood;
};


Item {
    BigState {
        id: bigState
        bigness: 61
        onGotFood: bigness = 99
    }

    Component.onCompleted: bigState.feed(3)
}

In order to be picked up by qmlinterfacegenerator, the declared class

  • must at least indirectly inherit from Qul::Object
  • must be default constructible

Some base classes have special meaning. For more detailed information, see Singletons and Models

Singletons

If the class derives directly from Qul::Singleton<Itself>, it is marked with pragma Singleton and an instance is available globally.

The following example demonstrates how to use Singletons:

#include <qul/singleton.h>

struct MyApi : public Qul::Singleton<MyApi>
{
    void start();
};


Item {
    Component.onCompleted: MyApi.start()
}

Models

The class can directly derive from from Qul::ListModel<T> to export a list model. See the Qul::ListModel documentation for details.

Functions

All public non-overloaded member functions in an exported class declaration are available in QML. The function's parameter types and return type are mapped to their QML counterparts.

The following example demonstrates how to use functions:

struct DirLookup : public Qul::Object
{
    Qul::Property<std::string> basePath;
    int lookup(const std::string &path);
};
struct Simulator : public Qul::Singleton<Simulator>
{
    void run(DirLookup *lookup);
};


Item {
    DirLookup {
        id: look
        basePath: "/objects"
    }

    Component.onCompleted: Simulator.run(look)
}

Properties

Public fields in exported class declarations become component properties if the field has the type Qul::Property<T>. Here T describes the property's C++ type and is mapped to a corresponding QML type.

Note: Every type T that do not have built-in comparison operator must be provided with a user-defined operator==.

The properties defined in this way behave like built-in properties. In particular they can be assigned bindings in QML and can be used as a data source in other bindings.

The following example demonstrates how to use properties. Define a property in C++:

struct MyData : public Qul::Object
{
    Qul::Property<int> val;
    void update(int x)
    {
        // can get and set property values from C++
        val.setValue(x);
    }
};

And use it in QML:

Item {
    Item {
        // can bind QML property to exported property
        x: mydata_x.val
        color: "red"
        width: 50
        height: 50
    }
    MyData {
        id: mydata_x
        val: 100
    }
    MyData {
        id: mydata_width
        // can bind exported property
        val: parent.width
    }
    Component.onCompleted: {
        mydata_x.update(200);
        console.log(mydata_width.val);
    }
}

Note: Calling Qul::Property::setValue does not request Qt Quick Ultralite engine update. Event queue is needed if there are no QML animations or timers running. Repainting can be triggered by using an event queue as shown below.

struct MySingleton : public Qul::Singleton<MySingleton>
{
    Qul::Property<int> val;

    // function that is called only from the C++ code
    void update(int value);
};

class MyEventQueue : public Qul::EventQueue<int>
{
    void onEvent(const int &value) override
    {
        // set property value in the event handler
        MySingleton::instance().val.setValue(value);
    }
};

void MySingleton::update(int value)
{
    static MyEventQueue myEventQueue;
    myEventQueue.postEvent(value);
}

Grouped properties

Properties can be grouped together as follows:

struct MyObject : public Qul::Object
{
    struct Grouped
    {
        Qul::Property<int> val1;
        Qul::Property<int> val2;
    };
    Grouped group;
};

Then they can be used in QML like this:

Item {
    MyObject {
        group.val1: 42
        group.val2: 43
    }
}

The grouping happens by placing the properties inside a struct or class S and then having a field of type S inside the exported class. The type S must not itself be derived from Qul::Object. Only its public fields of type Qul::Property are exposed as properties within the group. Groups are only for properties and can not have signals, enums or functions.

Signals

Public fields of type Qul::Signal<Fn> are translated to signals on the QML component.

The template argument Fn must be a function type that describes the signal's parameter types. As usual, these types are mapped to the matching QML types. Similarly, the parameter names used in Fn becomes a QML signal's parameter names.

The following code demonstrates how use signals:

struct MyItem : public Qul::Object
{
    Qul::Signal<void(int sigValue)> triggered;
    void callTriggered() { triggered(42); }
};


Item {
    MyItem {
        id: myitem
        onTriggered: console.log(sigValue);
    }
    Component.onCompleted: myitem.callTriggered()
}

Enums

Public enum declarations in an exported class declaration are translated to QML enums.

The following code demonstrates how use public enums:

struct MyStates : public Qul::Singleton<MyStates>
{
    enum State { On, Off, Broken };
};


Item {
    property MyStates.State state: MyStates.On
}

C++ to QML type mapping

C++ typeQML type
boolbool
integral (char, short, int, long, ...)int
floating point (float, double)real
std::string (assumed UTF-8 encoding)string
T* where T derives from Qul::Objectmatching component type
enum in exported classmatching enum type

Note: Conversion to std::string implies dynamic memory allocation.

Transferring data from Interrupt Handlers to QML

The following technique combines several APIs and mechanisms existing in Qt Quick Ultralite. In result, a straight forward and efficient data transfer from any part of the application (including interrupt handlers) is possible.

This example covers data transfer using the QML singleton pattern to expose data to C++, but the same technique is applicable for Objects and Models.

Note: interrupt_handler example demonstrates the techniques described here. You can find it under examples directory.

What's needed?

Start with defining your event type that will be used as a data payload.

struct HMIInputEvent
{
    enum Type { KeyPress, KeyRelease };

    int keyCode;
    Type type;
};

Next, implement a C++ to QML interface (for example Qul::Singleton). It can be combined with the Qul::EventQueue to change a property value or emit a signal when an event is received.

Example:

#include <qul/singleton.h>
#include <qul/eventqueue.h>

struct HMIInput : Qul::Singleton<HMIInput>, Qul::EventQueue<HMIInputEvent>
{
    Qul::Signal<void(int key)> pressed;
    Qul::Signal<void(int key)> released;

    void onEvent(const HMIInputEvent &inputEvent) override
    {
        if (inputEvent.type == HMIInputEvent::KeyPress) {
            pressed(inputEvent.keyCode);
        } else if (inputEvent.type == HMIInputEvent::KeyRelease) {
            released(inputEvent.keyCode);
        }
    }
};

This creates a Singleton object, which emits pressed and released signals on receiving the HMIInputEvent

Now, make use of the Singleton object in QML

import QtQuick 2.15

Rectangle {
    Row {
        spacing: 10
        Text { id: operation }
        Text { id: keyCode }
    }

    HMIInput.onPressed: {
        operation.text = "pressed"
        keyCode.text = key
    }

    HMIInput.onReleased: {
        operation.text = "released"
        keyCode.text = key
    }
}

At this point, the implementation is ready for transferring data by posting events to the EventQueue, for example from the interrupt handler.

Example:

static void keyEventInterruptHandler()
{
    static HMI_StateTypeDef keyBuffer;
    if (BSP_HMI_GetState(&keyBuffer) != HMI_OK) {
        return;
    }

    HMIInput::instance().postEvent(
        HMIInputEvent{keyBuffer.code,
                      keyBuffer.pressed ? HMIInputEvent::KeyPress : HMIInputEvent::KeyRelease});
}

Now, every time keyEventInterruptHandler() is called, a corresponding text in QML will change.

Available under certain Qt licenses.
Find out more.