Вступление
Один из самых крупных отделов нашей фирмы занимается разработкой приложений на 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.
Мы использовали 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 – это массив маркеров с необходимыми параметрами.
Мы можем добавлять маркеры, удалять, перемещать их по карте, запрашивать их параметры и получать от них отклик (например по щелчку мыши). Дальше мы ограничены только своей фантазией.
Из важного хочется отметить, что хотя в нашем примере мы использовали пиктограммы из интернета, вы всегда можете задействовать свои локальные файлы. Также хотелось бы отметить, что файл с одним и тем же именем, но другим контентом, использовать не получится. Для того чтобы значок на карте изменился, имя файла тоже должно быть другим.
Ссылка на исходный архив проекта находится ниже.