QtQuick Map Application (Part 1) : Adding a Layer Control

entry

QtQuick Map Application (Part 1) : Adding a Layer Control


Published on: 2023-08-10

In this tutorial, the first part in a series of three, you will learn how to modify the existing QtQuick sample and add a LayerControl to it.

We will replace the QML form to have a collapsible side panel, allowing to display a layer tree. The layer control will be populated from code through a model and will allow toggling on and off layers from the UI.

The extra resources needed to complete this tutorial, as well as the complete extended application, can be found here: QtQuickAppWithLayerControl.zip

Here’s what our starting point, the HelloWorldQtQuickSample, looks like:

And here’s the modified application at the end of this article:

Editing the QML

First, let’s swap the existing main.qml with the provided one. It now contains a side panel that can be expanded to display our layer control as well as a list of buttons to draw features on the map, which you will see in a later article.

The new form needs some icons, so let’s make sure to copy the corresponding files to your folder and add them to the project file CMakeLists.txt. The required files are:

eye.svg
eye-off.svg
layers.svg
pencil.svg

Below is what the QML code looks like. Note that this uses the QML TreeView type available since Qt 6.3. Feel free to experiment with the sample!

// QML TreeView
TreeView {
    anchors.fill: parent
    // The model needs to be a QAbstractItemModel
    model: layerModel

    delegate: Item {
        id: treeDelegate

        readonly property real rowIndex: row
        readonly property real columnIndex: column

        implicitWidth: padding + label.x + label.implicitWidth + padding
        implicitHeight: label.implicitHeight * 1.7

        readonly property real indent: 20
        readonly property real padding: 5

        // Assigned to by TreeView:
        required property TreeView treeView
        required property bool isTreeNode
        required property bool expanded
        required property int hasChildren
        required property int depth

        TapHandler {
            onTapped: treeView.toggleExpanded(row)
        }

        Text {
            id: indicator
            visible: treeDelegate.isTreeNode && treeDelegate.hasChildren
            width: treeDelegate.indent
            x: (treeDelegate.depth * treeDelegate.indent)
            padding: 8
            anchors.verticalCenter: label.verticalCenter
            text: "▸"
            rotation: treeDelegate.expanded ? 90 : 0
        }
        CheckBox {
            id: control
            width: treeDelegate.indent
            x: (treeDelegate.depth + 1) * treeDelegate.indent
            checked: model.checkState
            hoverEnabled: false
            background: Rectangle {
                color: alternateColor
            }
            anchors.verticalCenter: parent.verticalCenter
            indicator: ColorImage {
                x: (parent.width - width) / 2
                y: (parent.height - height) / 2
                source: control.checkState === Qt.Checked ? "eye.svg" : "eye-off.svg"
            }
            onCheckedChanged: {
                layerModel.onCheckedChanged(model.treeIndex, checked)
            }
        }
        Text {
            id: label
            x: treeDelegate.padding + (treeDelegate.isTreeNode ? (treeDelegate.depth + 2) * treeDelegate.indent : 0)
            width: treeDelegate.width - treeDelegate.padding - x
            clip: true
            text: model.display
            anchors.verticalCenter: parent.verticalCenter
        }
    }
}

The QML handles styling of the TreeView but also provides hooks the code can attach to, in order to provide the data model used to populate the tree as well as application logic when interacting with the items. Let’s have a closer look at a couple lines.

// Model binding
model: layerModel

This lets us create our LayerModel class in code, implementing QAbstractItemModel, and provide it to the QtQuick context. For simplicity, this example will subclass QStandardItemModel.

// Two-way binding
checked: model.checkState

onCheckedChanged: {
    layerModel.onCheckedChanged(model.treeIndex, checked)
}

These lines allow a two-way binding between the model and the checkbox delegates symbolizing our tree nodes and leaves. The first line reflects the model values in the UI while the second one forwards user changes to our model.

If we try running the sample at this stage, we will run into the following error: “qrc:/main.qml:44: ReferenceError: layerModel is not defined”. This error comes from layerModel being referenced without being defined, as it will be provided from code.

However, for educational purposes, we can simply comment out the line and see the outcome, which is an empty layer control.

Layer Model Creation

Let’s create a LayerModel instance, so we can set it as a context property. We will subclass QStandardItemModel, and set a Layer pointer as data to QStandardItems by registering our pointer as a QMetaType. Let’s create two files: layercontrol.h and layercontrol.cpp.

// layermodel.h
// Model class inheriting from QStandardItemModel.
class LayerModel : public QStandardItemModel
{
    Q_OBJECT

public:
    void initialize();

    Q_INVOKABLE void onCheckedChanged(const QModelIndex& index, const bool checked);
    Q_INVOKABLE bool isChecked(QModelIndex index);

    enum LayerModelRoles{
        LayerRole = Qt::UserRole + 1,
        TreeIndexRole
    };

protected:
    virtual QHash<int, QByteArray> roleNames() const;

private:
    void addChildLayerNodes(LayerCollectionPtr layerSet, QStandardItem* rootNode);
};

The context property needs to be set before loading the source QML using the QQmlApplicationEngine. However, it is also necessary for our QQmlApplicationEngine to have been initialized in order to retrieve the QQmlContext. Therefore, we will create the layer model and initialize it after our controller is initialized, so we can access the Carmenta Engine View and its layers.

// LayerModel initialization
// Initialize the application's GUI.
QQmlApplicationEngine engine;
const QUrl url("qrc:/HelloWorldQtQuick/main.qml");
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                &app, [url](QObject *obj, const QUrl &objUrl) {
    if (!obj && url == objUrl)
        QCoreApplication::exit(-1);
}, Qt::QueuedConnection);

// Create a layer model before loading the QML
LayerModel model{};
QQmlContext* ctxt = engine.rootContext();
QStandardItemModel* itemModel = &model;
ctxt->setContextProperty("layerModel", itemModel);

engine.load(url);
if (!engine.rootObjects().isEmpty())
{
    // Connect the QML scene with the controller singleton.
    QList<QObject*> rootObjects = engine.rootObjects();
    Controller::instance().initialize(rootObjects.first());
    model.initialize(); // This populates the layer model

    // Show application and wait for it to exit.
    result = app.exec();
}

Now, when we run the application, the context is being provided, however empty. Let’s first add two utility methods to our controller:

  • ViewPtr view() to access the view (as we will need to access its layers)
  • void updateView() to update the view through the mapControl, which is needed after enabling or disabling a layer.
Utility methods in controller.h
Carmenta::Engine::ViewPtr view()
{
    return mapControl->view();
}

void updateView()
{
    return mapControl->updateView();
}

Finally, let’s populate our model. In the initialize method, you can retrieve the layers from the view through the controller. Then, it is possible to call addChildLayerNodes which will go through the layers recursively, create items, and add them to the correct place in the tree.

Layer model initialization
void LayerModel::initialize()
{
    ViewPtr view = Controller::instance().view();
    addChildLayerNodes(view->layers(), this->invisibleRootItem());
}

QHash<int, QByteArray> LayerModel::roleNames() const
{
    QHash<int, QByteArray> result= QStandardItemModel::roleNames();

    result[Qt::CheckStateRole] = "checkState";
    result[LayerRole] = "layer";
    result[TreeIndexRole] = "treeIndex";

    return result;
}

Let’s have a look at how we can initialize our model with the view’s layers. In the addChildLayerNodes method, our nodes and leaves are represented by QStandardItems, with a Layer as data. LayerSets, inherited by TileLayers, will correspond to group nodes and other layers, mainly OrdinaryLayers, to leaves. The LayerPtr will be stored and accessed through the data member of the QStandardItem class. The recursive function takes all children of a layer and the corresponding node, appends them to said node and recursively runs the function on all children layers if they have children of their own. Each QStandardItem receives the display name, or regular name as default, of the layer and a pointer to the layer. Its checkState will also get set depending on the layer’s enabled state.

Population LayerModel
void LayerModel::addChildLayerNodes(LayerCollectionPtr layerSet, QStandardItem* rootNode)
{
    // Iterating on all children of the current node. All the children are on the same level.
    for (uint32_t i = 0; i < layerSet->size(); i++)
    {
        LayerPtr layer = layerSet->get(i);
        QStandardItem* layerItem = new QStandardItem(0);

        AttributeValue attr;

        String name = layer->name().c_str();
        String displayName = layer->displayName().c_str();
        if (displayName != " " && displayName != "")
            name = displayName;
        layerItem->setText(name.c_str());

        QVariant data; 
        data.setValue(layer);
        layerItem->setData(data, LayerRole);

        layerItem->setToolTip(layer->description().c_str());

        rootNode->appendRow(layerItem);

        // Store index to make it accessible in QML
        layerItem->setData(layerItem->index(), TreeIndexRole);

        layerItem->setCheckable(true);
        layerItem->setCheckState(layer->enabled() ? Qt::Checked : Qt::Unchecked);

        // If the current child can be cast to a LayerSet (which layers with several children inherit from), the function is run again on its children. 
        LayerSetPtr cLayer = dynamic_cast<LayerSet*>(layer.get());
        if (cLayer != 0)
            addChildLayerNodes(cLayer->layers(), layerItem);
    }
}

It is then possible to communicate the checkState status of each item through the isChecked method. The QStandardItem can be retrieved by using the QStandardItemModel::itemFromIndex method on the QModelIndex. An element will only be retrieved if it has a valid model index.

Forwarding model states to the UI
bool LayerModel::isChecked(QModelIndex index)
{
    // Root element's index is invalid. 
    // Default value is false but it doesn't matter as it isn't visualized.
    if (index.isValid())
        return this->itemFromIndex(index)->checkState() == Qt::Checked;
    return false;
}

Finally, whenever the user interacts with a layer, the corresponding layer should be toggled as well as its corresponding checkState. This is done by setting the checkState and accessing the layer through its pointer stored in the QStandardItem::data() member.

After toggling the layer, which is a thread-safe operation meaning you don’t need to use any Guard, don’t forget to update the view to reflect the changes.

Registering user changes in the model
void LayerModel::onCheckedChanged(const QModelIndex& index, const bool checked)
{
    // Get the LayerItem corresponding to the model index.
    QStandardItem* layerItem = this->itemFromIndex(index);
    if (!layerItem)
        return;

    // Keeping the model in sync with the UI
    layerItem->setCheckState(checked ? Qt::Checked : Qt::Unchecked);

    // Toggle the layer enabled state
    LayerPtr layer = layerItem->data(LayerRole).value<LayerPtr>();
    layer->enabled(checked);

    // Update the view.
    Controller::instance().updateView();
}

We have now extended our application sample with a layer panel to control the layers of our map. Launch the application to see the result of the first part of the QtQuick article series.