Этот пост участвует в конкурсе „Умные телефоны за умные посты“.
Попробуем решить следующую задачу: показать в приложении карту с пинами, то есть сделать стандартную функциональность, нужную для любого LBS приложения. Причем, сделать это в парадигме MVC – то есть модель данных и контроллер в C++, а QML занимается только отображением и логикой, связанной с UI. Для карты будем использовать использовать стандартный элемент Map, а заодно разберемся со связыванием модели данных из C++ и QML.
Для работы нужен установленный Qt SDK 1.1.4. Создаем новый проект: Qt Quick Application -> Qt Quick Components for Meego/Harmattan, далее все по дефолту. Поскольку мы будем использоваться Qt Mobilitiy, надо добавить в pro файл:
CONFIG += mobility
После этого имеем пустой проект с некой обвязкой на C++ и два qml файла – main.qml и MainPage.qml. Main мы трогать не будем, в нем лежит стандартные для платформы верхний статус бар и нижнее меню. В MainPage нужно импортировать нужное:
import QtMobility.location 1.2
и удалить все внутри элемента Page, заменив это на Map:
Map {
id: map
anchors.fill:parent
plugin: Plugin {name:"nokia"}
zoomLevel: 13
center: Coordinate { latitude: 55.755786; longitude: 37.617633 }
}
Обращу внимание на определение Plugin – так надо писать, чтобы получить в карте тайлы от Nokia (Ovi) Maps. Есть возможность подключить другие плагины, но из коробки их нет. Далее мы определяем уровень приближения и координаты центра карты. Запускаем, и видим Москву.
Все хорошо, но нам нужен хотя бы скролл пальцем. Нативный контрол это делать не умеет, поэтому делаем сами. Поверх карты добавляем MouseArea которая будет отлавливать одиночные тачи и двигать карту:
MouseArea {
anchors.fill:parent
property int lastX : -1
property int lastY : -1
onPressed : { lastX = mouse.x; lastY = mouse.y; }
onReleased : { lastX = -1; lastY = -1; }
onPositionChanged: {
if (lastX>=0) {
map.pan(lastX- mouse.x, lastY - mouse.y)
lastX = mouse.x
lastY = mouse.y
}
}
}
С зумом двумя пальцами можно разобраться аналогичным способом, используя элемент PinchArea.
Теперь надо нарисовать пины. Контрол карты поддерживает отрисовку в себе различных элементов, как графически примитивов, так и битмапов, но динамически это можно делать через методы addMapObject и removeMapObject, что не очень согласуется с нашим желанием делать MVC. Еще один вариант, использовать MapObjectView, но, как говорит документация «For model data, currently only LandmarkModel is supported. Using other types of models results in undefined behavior.» А LandmarkModel – это темный лес с кучей ограничений и ненужных нам действий. Нам надо что-то попроще. Так что мы пойдем своим путем, но сначала разберёмся с моделью в С++.
Модель будет проста: внутри будет список пинов, у каждого lat, lng и imageSource для url картинки для пина. Но во view мы будем отдавать уже не lat и lng, а абсолютные значения X и Y пина на экране. Также реализуем метод AddPin, которым будем добавлять пины в список, и DrawPins который будет вызываться из view при каких-то изменениях и пересчитывать значения X и Y. DrawPins должен быть объявлен как public slot, чтобы уметь принимать сигналы из view.
class PinList : public QAbstractListModel
//…
void PinList::addPin(QString imageSource, double lat, double lng)
{
//создаем внутренний пин с реальными координатами
Pin *pin = new Pin(this);
pin->imageSource = imageSource;
pin->Lat = lat;
pin->Lng = lng;
beginInsertRows(QModelIndex(), this->Pins->length(), this->Pins->length());
this->Pins->append(pin);
endInsertRows();
//говорим view, что данные изменились
emit dataChanged(createIndex(0,0),createIndex(this->Pins->size(),0));
}
//view передает нам текущие координаты углов в lat, lng и в x,y (считаем тут что один угол у нас в 0,0)
void PinList::DrawPins(QString x,QString y,QString x_end,QString y_end, QString map_x, QString map_y)
{
//запоминаем переданные координаты
map_first_lat = x.toDouble();
map_first_lng = y.toDouble();
map_second_lat = x_end.toDouble();
map_second_lng = y_end.toDouble();
map_x_end = map_x.toDouble();
map_y_end = map_y.toDouble();
//говорим view, что данные изменились
emit dataChanged(createIndex(0,0),createIndex(this->Pins->size(),0));
}
//вызывается из view для запроса полей модели
//тут как раз пересчитываем из lat,lng в x,y
QVariant PinList::data(const QModelIndex & index, int role) const {
if (index.row() < 0 || index.row() > this->Pins->count())
return QVariant();
const Pin* pin = this->Pins->at(index.row());
QVariant result;
switch (role) {
case ImageSource:
result = QVariant(pin->imageSource);
break;
case X:
if ( pin->Lng > map_first_lng && pin->Lng < map_second_lng)
{
result =QVariant((map_x_end)*(pin->Lng - map_first_lng)/(map_second_lng - map_first_lng));
}
break;
case Y:
if ( pin->Lat > map_second_lat && pin->Lat < map_first_lat)
{
result = QVariant(map_y_end*(pin->Lat - map_first_lat)/(map_second_lat - map_first_lat));
}
break;
}
return result;
}
Теперь надо нарисовать все это во view. Для этого определим, как выглядит пин:
//пин
Component {
id: landmarkMapDelegate
Item {
id:land
width: 20;
height: 20 ;
x: X
y: Y
Image {
source:ImageSource
}
}
}
и нарисуем Repeater поверх карты
Item {
anchors.fill:parent
Repeater {
model: pinlist
delegate: landmarkMapDelegate
}
}
Вот тут важно: model привязано к идентификатору pinlist. Именно этот идентификатор нужно будет потом указать для привязки модели в C++.
Объявим функцию DrawPins, которая будет считать lat и lng углов карты, и передавать ее в модель
function drawPins() {
var topLeft = map.toCoordinate(Qt.point(0,0))
var bottomRight = map.toCoordinate(Qt.point(map.width, map.height))
pinlist.DrawPins(topLeft.latitude,topLeft.longitude,bottomRight.latitude,bottomRight.longitude, map.width,map.height);
}
И вызовем ее в OnPositionChanges карты.
Теперь осталось, собственно, связать нашу модель и View:
//создаем модель
PinList* pinList = new PinList(0);
//добавляем несколько пинов
pinList->addPin("qrc:/icons/pin.png", 55.745, 37.6175);
pinList->addPin("qrc:/icons/pin.png", 55.7575, 37.619697);
pinList->addPin("qrc:/icons/pin.png", 55.751667, 37.617778);
//связываем модель и вью
viewer->rootContext()->setContextProperty("pinlist", pinList);
//читаем qml
viewer->setMainQmlFile(QLatin1String("qml/untitled/main.qml"));
Все.
Исходники