Блог

Qt and QML maps

Сен 21, 2021

Вступление

Один из самых крупных отделов нашей фирмы занимается разработкой приложений на C++ с использованием широко известной библиотеки Qt. В основном наши приложения создаются на основе QWidget-классов при помощи UI-дизайна, но недавно перед нами встала задача интеграции и использования QML-классов и компонентов.

QML (Qt Modeling Language) is a user interface markup language. It is a declarative language (similar to CSS and JSON) for designing user interface–centric applications. Inline JavaScript code handles imperative aspects. It is associated with Qt Quick, the UI creation kit originally developed by Nokia within the Qt framework. Qt Quick is used for mobile applications where touch input, fluid animations and user experience are crucial. A QML document describes a hierarchical object tree. QML modules shipped with Qt include primitive graphical building blocks (e.g., Rectangle, Image), modeling components (e.g., FolderListModel, XmlListModel), behavioral components (e.g., TapHandler, DragHandler, State, Transition, Animation), and more complex controls (e.g., Button, Slider, Drawer, Menu). These elements can be combined to build components ranging in complexity from simple buttons and sliders, to complete internet-enabled programs.
QML elements can be augmented by standard JavaScript both inline and via included .js files. Elements can also be seamlessly integrated and extended by C++ components using the Qt framework.

https://en.wikipedia.org/wiki/QML

Мы использовали QML в несколько необычной роли: вместо создания интерфейса мы применили его для работы с картами.

Давайте рассмотрим основные технические моменты и приёмы, которые мы использовали, а также продемонстрируем примеры и основы взаимодействия базового кода на Qt с QML-объектами.

Где хранить

QML-классы и функции обычно хранятся в файлах с расширением .qml. Их можно поместить как рядом с программой, так и где-то на вашем сервере. Подгружать можно по сети. Мы же встраиваем свои QML непосредственно в ресурсы. То есть они внедрены в исполняемый файл и не могут быть «подкручены» пользователем.

Для загрузки такого «.qml» используется: setSource(QUrl::fromLocalFile(«:/map»))

Как встроить

Для начала нам было необходимо встроить некий QML viewer в одно из окошек нашего приложения. Мы использовали следующий код:

_qml_view = new QQuickView();
QWidget* container = QWidget::createWindowContainer(_qml_view, this);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
_ui->qml_canvas_layout->addWidget(container);
_qml_view->setSource(QUrl::fromLocalFile(":/map"));

Рассмотрим самый простой пример QML-объекта:

import QtQuick 2.0

Rectangle {
   id: main
   color: "#FF0000"
}

Пока мы не зададим размеры прямоугольника, мы ничего не увидим. Добавим атрибуты width и height и получим что-то примерно вот такое:
import QtQuick 2.0

import QtQuick 2.0

Rectangle {
   id: main
   color: "#FF0000"
   width: 200
   height: 100
}

Отлично! Простой пример сработал. Теперь этот «задник» нашего будущего компонента хотелось бы растянуть на всю область. Ведь наше окно QMainWidget может динамически менять свой размер, а значит и размер области, выделенной под QML-объект, тоже будет меняться. Здесь нам на помощь приходят атрибуты «родительского» компонента.
import QtQuick 2.0

import QtQuick 2.0
Rectangle {
   id: main
   color: "#FF0000"
   width: parent.width
   height: parent.height
}

Отлично! То, что надо!

Плагин карты

В QML можно в виде плагинов встраивать различные специально подготовленные библиотеки. Вот и в нашем случае мы используем встроенный в библиотеку Qt плагин для отображения карты.

import QtQuick 2.0
import QtLocation 5.11
import QtPositioning 5.11

Rectangle {
   id: main
   width: parent.width
   height: parent.height

   Plugin {
       id: mapPlugin
       name: "esri"
   }

   Map {
       id: mapView
       objectName: “mapView”
       anchors.fill: parent
       plugin: mapPlugin
       center: QtPositioning.coordinate(59.9386, 30.3141)
       zoomLevel: 15
   }
}

Теперь можно общаться с этим QML-объектом.

Что мы можем делать с картой

Мы можем считывать параметры карты и менять их по своему усмотрению. Приведём пример считывания координат центра и установки нового центра. Для доступа к объектам QML из кода программы мы используем их «имена», заданные в objectName, и работаем через интерфейс QQuickItem.

auto map_view = _qml_view->rootObject()->findChild<QQuickItem*>("mapView");
auto coordinates = map_view->property("center").value<QGeoCoordinate>();
coordinates.setLatitude(coordinates.latitude() + 0.02);
map_view->setProperty("center", QVariant::fromValue<QGeoCoordinate>(coordinates));

Мы можем вызывать функции, описанные в QML. Покажем это на примере получения координат точки, где находится курсор мыши.

Для получения координат добавим в QML структуру и метод:

MouseArea {
  id: mapViewMouseArea
  anchors.fill: parent
  propagateComposedEvents: true
  hoverEnabled: true
}

function getMousePosition() {
 return mapView.toCoordinate(Qt.point(mapViewMouseArea.mouseX, mapViewMouseArea.mouseY));
}

Для вызова getMousePosition из C++ используем QMetaObject::invokeMethod:

std::tuple<float, float> MainWindow::getMouseCoordinates()
{
   auto map_view = _qml_view->rootObject()->findChild<QQuickItem*>("mapView");
   QVariant result;
   bool invoke_result = QMetaObject::invokeMethod(map_view, "getMousePosition", Qt::DirectConnection, Q_RETURN_ARG(QVariant, result));
   if(!invoke_result)
       std::make_tuple(0.f, 0.f);
   QGeoCoordinate coordinates = result.value<QGeoCoordinate>();
   return std::make_tuple(coordinates.latitude(), coordinates.longitude());
}

Теперь покажем как из QML передать данные в QWidgets.

Добавим в QML событие изменения центра и в нём будем вызывать метод некоего объекта:

onCenterChanged: {
  qmlReceiver.centerChanged(center);
}

В «.cpp» файле опишем объект и свяжем его с QML:

class QMLReceiver : public QObject
{
private:
   Q_OBJECT

public:
   Q_INVOKABLE void centerChanged(QGeoCoordinate coordinate)
   {
       emit centerChangedSignal(coordinate.latitude(), coordinate.longitude());
   }

signals:
   void centerChangedSignal(float lat, float lon);
};

   _qml_receiver = new QMLReceiver();
   _qml_view->rootContext()->setContextProperty("qmlReceiver", _qml_receiver);
   QObject::connect(_qml_receiver, &QMLReceiver::centerChangedSignal, this, &MainWindow::onCenterChanged);

Обратите внимание на определение метода, вызываемого из QML, и на связывание объектов через setContextProperty.

Теперь приступим к самому интересному, то есть к размещению объектов на карте.

Для отображения объектов на карте используется концепция Model-View. В объект Map добавим:

MapItemView {
           model: markerModel
           delegate: mapComponent
       }

Где markerModel будет С++ классом class MarkerModel final : public QAbstractListModel, связанным с QML таким же образом, как и слушатель событий ранее _qml_view->rootContext()->setContextProperty(«markerModel», _model). При этом mapComponent описан так:

Component {
       id: mapComponent

       MapQuickItem
       {
           id: marker
           anchorPoint.x: image.width/2
           anchorPoint.y: image.height
           coordinate: positionValue

           property string identifier: identifierValue
           property string name: nameValue
           property string icon: iconValue

           sourceItem: Image {
               id: image
               source: icon;
           }

           MouseArea {
               id: mouseArea
               anchors.fill: parent
               hoverEnabled: true
               drag.target: parent

               onClicked: {
                   qmlReceiver.markerClicked(identifier, name, coordinate);
               }
           }
       }

Здесь мы видим настраиваемые property с данными, которые мы должны связать с нашей моделью:

  • positionValue, из которой берутся координаты объекта;
  • identifierValue и nameValue, позволяющие идентифицировать объект;
  • iconValue, определяющая значок для отображения объекта.

Связывание настраиваемых property с моделью осуществляется следующим образом:

enum MarkerRoles
{
    positionRole = Qt::UserRole + 1,
    identifierRole = Qt::UserRole + 2,
    nameRole = Qt::UserRole + 3,
    iconRole = Qt::UserRole + 4,
};

QHash<int, QByteArray> MarkerModel::roleNames() const
{
   QHash<int, QByteArray> roles;
   roles[positionRole] = "positionValue";
   roles[identifierRole] = "identifierValue";
   roles[nameRole] = "nameValue";
   roles[iconRole] = "iconValue";
   return roles;
}

QVariant MarkerModel::data(const QModelIndex& index, int role) const
{
   if(index.row() < 0 ||
      index.row() >= _markers.count())
       return QVariant();
   if (role == MarkerModel::positionRole)
       return QVariant::fromValue(_markers[index.row()]._position);
   else if (role == MarkerModel::identifierRole)
       return QVariant::fromValue(_markers[index.row()]._identifier);
   else if (role == MarkerModel::nameRole)
       return QVariant::fromValue(_markers[index.row()]._name);
   else if(role == MarkerModel::iconRole)
       return QVariant::fromValue(_markers[index.row()]._icon);
   return QVariant();
}

Где _markers – это массив маркеров с необходимыми параметрами.

Мы можем добавлять маркеры, удалять, перемещать их по карте, запрашивать их параметры и получать от них отклик (например по щелчку мыши). Дальше мы ограничены только своей фантазией.

Из важного хочется отметить, что хотя в нашем примере мы использовали пиктограммы из интернета, вы всегда можете задействовать свои локальные файлы. Также хотелось бы отметить, что файл с одним и тем же именем, но другим контентом, использовать не получится. Для того чтобы значок на карте изменился, имя файла тоже должно быть другим.

Ссылка на исходный архив проекта находится ниже.

Download

Новое в блоге

Первый курс на платформе GRSE TalentLab: Как мы обучали Angular с нуля

В июле 2024 года завершился первый онлайн-курс на базе нашей новой образовательной платформы - “GRSE TalentLab”. Курс был посвящен основам технологии Angular. Для удобства он был разделен на две части: подготовительную и основную. Подготовительный курс Цель...

Разработка через прототипирование

Разработка через прототипирование помогает команде разработчиков исключить недопонимание на всех уровнях, задействуя прототипы на каждом из них в процессе эволюции проекта.

Современный подход к подготовке технической документации

Мир информационных технологий находится в постоянном развитии. Вместе с ним совершенствуются системы по созданию и поддержке технической документации. Предлагаем краткое знакомство с возможностями современных систем в данной сфере.