Pull to refresh

Связывание модели данных в C++ c представлением в QML на примере карты

Reading time4 min
Views5.4K


Этот пост участвует в конкурсе „Умные телефоны за умные посты“.

Попробуем решить следующую задачу: показать в приложении карту с пинами, то есть сделать стандартную функциональность, нужную для любого 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"));


Все.

Исходники

Tags:
Hubs:
Total votes 22: ↑11 and ↓110
Comments7

Articles