jeudi 26 janvier 2012

Qt quick components Symbian : combiner TabBar et PageStack

Bonjour,
Aujourd'hui je vais vous présenter un exemple de code définissant un squelette d'application Qt basée sur les composants Qt Quick et combinant les contrôles TabBar (barre à onglets) et PageStack (empilement de page). Le rendu sera le suivant :



Comme vous pouvez le constater, l'application est composée (de bas en haut) :
  • une barre de status
  • une bannière
  • une barre à deux onglets
  • une page contenant un libellé et un bouton
  • une barre d'outils
L'expérience utilisateur est la suivante :
  • si je clique sur la flèche en bas à gauche, je quitte l'application car la profondeur de la pile de pages est de 1
  • si je clique sur le bouton "Go to Page 1.1", j'affiche une nouvelle page tout en restant sur l'onglet "Tab 1" ; cet onglet contiendra alors une pile de pages de profondeur 2 :


Note : si je clique sur la flèche "retour", je retournerai à la page précédente tout en restant sur     l'onglet Tab1
  • si je clique sur le bouton "Go to Page 1.2", j'affiche une nouvelle page tout en restant sur l'onglet "Tab 1" ; cet onglet contiendra alors une pile de pages de profondeur 3 :

Pour revenir à la page initiale 1.0, il y a deux options : soit je clique sur l'onglet "Tab1" lui-même, soit je clique successivement deux fois sur la flèche "retour".
  • enfin si je clique sur l'onglet "Tab2", j'affiche une simple page :


Note : si je clique sur "retour", l'application se termine car la pile de page ne contient qu'une seule page.

Regardons maintenant le code de plus près ... J'ai crée un projet Qt quick sous Qt Creator basé sur l'utilisation des composants Qt Quick pour symbian. Le projet sera constitué de 5 fichiers QML :
  • fichier main.qml définissant visuellement le squelette de l'application avec la barre de status, la bannière, les onglets et la barre d'outils
  • fichier splashscreen.qml affichant une image au lancement
  • fichier Page1_0.qml définissant la première page de l'onglet 1
  • fichier Page1_1.qml définissant la seconde page de l'onglet 1
  • fichier Page1_2.qml définissant la troisième et dernière page de l'onglet 1
Note : la définition de l'unique page de l'onglet n°2 sera embarquée dans le fichier main.qml.

Fichier main.qml

J'ai créé une barre de status car le contenu de l'application ne nécessite pas une immersion totale comme cela peut être le cas avec un player video :

Window{
    id: rootWindow

    property Item mySplashscreen;
    property int zOrder:12;


        // Barre de status masquée par défaut
    StatusBar {
        id:statusbar
        anchors.top: parent.top
        z:zOrder
        visible:false
    }
 
Cette barre de status est masquée par défaut le temps d'afficher le splashscreen en plain écran. Par ailleurs j'ai donné une profondeur importante (facteur z) de manière à assurer qu'aucun composant puisse masquer cette barre. Ensuite j'ai défini une bannière placer sous la barre de status :

// Bannière d'entête masquée par défaut
Image{
    id: header
    anchors {
        left: parent.left
        right: parent.right
        top: statusbar.bottom
    }
    source: "header.png"
    width:360
    height:49
    z:zOrder
    visible:false
}


Dans une véritable application, cette bannière pourra représenter une marque ou une bannière de publicité. Puis j'ai défini une barre avec deux onglets :


// Barre d'onglets (Tabbar) contenant trois onglets dans notre cas
TabBarLayout {
     id: tabBarLayout
     anchors {
         left: parent.left
         right: parent.right
         top: header.bottom
     }
     z: zOrder
     visible: false
     TabButton { tab: tab1; text: "Tab1"; onClicked: tab1.pop(null)}
     TabButton { tab: tab2; text: "Tab2" }
 }

 // Définition du contenu de chaque onglet
 TabGroup {
     id: tabGroup
     anchors {
         top: tabBarLayout.bottom
         left: parent.left
         right: parent.right
         bottom: parent.bottom
     }

     // Onglet n°1 contenant une pagestask
     PageStack {
         id: tab1

         initialPage: Page1_0 {tools: toolBarLayout1}

         ToolBar {
             id: myToolBar1
             anchors.bottom: parent.bottom
             tools:toolBarLayout1
             z:zOrder
         }

         ToolBarLayout {
             id: toolBarLayout1
             ToolButton {
                 flat: true
                 iconSource: "toolbar-back"
                 onClicked: {
                     if(tab1.depth <= 1) Qt.quit()
                     else
                         tab1.pop()
                 }
             }
         }
     }

     // Onglet n°2 contenant une simple page
     Page {
         id: tab2
         anchors.fill: parent

         Rectangle
         {
             color:"darkgreen"
             anchors.fill: parent
             Text {
                 id:title
                 anchors.centerIn: parent
                 text: "Simple page"
                 color: "white"
             }

         }

         tools: toolBarLayout2

         ToolBar {
             id:myToolBar2
             anchors.bottom: parent.bottom
             tools:toolBarLayout2
             z:zOrder
         }

         ToolBarLayout {
             id: toolBarLayout2
             ToolButton {
                 flat: true
                 iconSource: "toolbar-back"
                 onClicked: Qt.quit()
             }
         }
     }
  }
 
Comme vous pouvez le constater, le premier onglet "tab1" est représenté par une pile de pages (composant PageStack) dont la page initiale est définie dans un autre fichier (Page1_0.qml) tandis que le second onglet "tab2" est représenté par une simple page définie en ligne. C'est un choix délibéré afin de vous montrer qu'il est possible de gérer différents types de contenu par onglet. Par ailleurs chaque "contenant" d'onglet disposera d'une barre d'outils constituée d'un bouton de retour. Dans le cas d'une pile de pages, un clic sur "retour" permettra de dépiler la pile afin d'afficher la page précédente. Dans le cas d'une page simple, le même clic aura pour effet de quitter l'application. Dernier point : noter la présense d'un slot "onclicked" pour le premier TabButton : tab1.pop(null). Celui-ci permet de dépiler toute la pile afin de retourner à la page racine.

Component.onCompleted:
{
    // Création et affichage du splashscreen
    var component = Qt.createComponent(Qt.resolvedUrl("Splashscreen.qml"));
    if (component.status == Component.Ready)
    {
        mySplashscreen = component.createObject(rootWindow);
    }
}


Une fois la page principale chargée, je crée dynamiquement le splashscreen dont nous gardons une instance dans la propriété mySplashscreen. Enfin j'initialise un timer de 3 secondes, durée d'affichage du splashscreen :

// Définit la durée d'affichage du splashscreen (3s)
Timer {
      interval: 3000;
      running: true;
      onTriggered:
      {
          mySplashscreen.visible = false
          tabBarLayout.visible = true
          statusbar.visible = true
          header.visible = true
      }
}


Sur expiration du timer, je masque le splashscreen et je rends visible la bannière, la barre de status et la barre d'onglets.

Le code complet est disponible ici.

mardi 10 janvier 2012

Détecter les changements de position GPS depuis une application hybride Qt/HTML

Bonjour,
Dans cet article, je vais vous présenter une manière simple d'afficher en temps réel la position GPS (latitude et longitude) du téléphone ; cette position sera biensûr mis à jour automatiquement si l'utilisateur se déplace. Il est tentant d'utiliser l'api HTML5 de géolocalisation (navigator.geolocation.watchPosition). Malheurseusement celle-ci n'est pas supportée dans l'implementation Qt Webkit pour Symbian^3 et ses déclinaisons. Je vais donc vous exposer une méthode de contournement répondant à ce besoin.

Tout d'abord je vous invite à créer un projet Qt HTML5 : pour cela cliquer sur "Fichier / Nouveau" sous Qt Creator et sélectionner dans Projets la section "Autre projet" puis "Application HTML5" comme indiqué sur la capture ci-dessous :



Une fois le projet crée, un certain nombre de fichiers a été généré :
  • un ficher html dénommé "index.html" contenant la page web d'entrée de votre application
  • un fichier main.cpp constituant le point d'entrée de l'application Qt
  • un fichier html5applicationviewer.cpp en charge de gérer la webview native utilisée pour piloter votre application web
Pour arriver à mes fins, je vais devoir modifier les fichiers index.html et html5applicationviewer.cpp avec l'idée de créer un objet en C++ faisant le lien entre le monde web (fichier index.html) et le monde natif (fichier html5applicationviewer.cpp). Cet objet natif exposera par le biais de signaux et slots les apis permettant à la partie web de récupérer les notifications de changement de position. Cet objet C++ est défini dans un fichier dénommé cppobject.cpp/.h :

using namespace QtMobility;

class CppObject : public QObject
{
    Q_OBJECT

public:
    CppObject(QWebFrame *frame);

public slots:
    void displayTrace( const QString &param);
    void startWatchingPosition();

signals:
    void positionChanged(double lat, double lng);

private slots:
    void attachObject();
    void positionUpdated(const QGeoPositionInfo &info);

private:
    QWebFrame *m_frame;
    QGeoPositionInfoSource * m_geoPositionInfoSource;

};

Les slots publics représentent les méthodes appelables directement depuis le code html (ici une méthode pour sortir des traces dans la console et une méthode pour s'enregistrer comme listener de changement de position). L'unique signal "positionChanged" sera émis à chaque modification de la position physique du téléphone. Le slot privé "attachObject" permet de déclarer mon objet natif dans le runtime d'éxécution Qt Webkit. Le second slot privé "positionUpdated" est associé à l'api Qt Mobility location afin de récupérer en temps réel la position physique. Enfin cet objet dispose de deux propriétés : "m_frame" représentant la webview dans laquel l'objet C++ est exposé et "m_geoPositionInfoSource" représentant le gestionnaire des mise à jour de position de l'api QTM Location.

Passons maintenant à l'implementation de cet objet :

CppObject::CppObject(QWebFrame *frame)
{
    m_frame = frame;

    if(m_frame)
    {
        attachObject();
        connect( m_frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(attachObject()) );
    }

    mpGeoPositionInfoSource = QGeoPositionInfoSource::createDefaultSource(this);
    mpGeoPositionInfoSource->setPreferredPositioningMethods(QGeoPositionInfoSource::AllPositioningMethods);
    connect(mpGeoPositionInfoSource, SIGNAL(positionUpdated(QGeoPositionInfo)), SLOT(positionUpdated(QGeoPositionInfo)));
}

Je conserve une référence sur la webview parent et je rattache mon instance courante à cet frame web via la méthode privée attachObject :

void CppObject::attachObject()
{
    m_frame->addToJavaScriptWindowObject( QString("CppObject"), this );
}

Ainsi je pourrais utiliser une référence à "CppObject" depuis html. Dans la suite du constructeur, je connecte le signal javascriptWindowObjectCleared à mon slot attachObject. Ceci me permet de garder la référence de mon objet côté javascript y compris après chargement de nouvelles urls. Ensuite je fais appel à l'api QTM Location en utilisant la source de géolocalisation par défaut en indiquant que toutes les méthodes de positionnement peuvent être utilisées. Enfin je connecte un slot privé pour que mon objet soit notifiée dès qu'un changement de position intervient.

Je définis ensuite mes deux slots publics exposables au html :

void CppObject::displayTrace( const QString &param)
{
    qDebug() << param;
}

Simple méthode de sortie console.

void CppObject::startWatchingPosition()
{
    mpGeoPositionInfoSource->startUpdates();
}

Slot public permettant d'initier le monitoring de changement de position.

Enfin mon slot privé pour gérer les notifications de changement de position remontées par QTM Location :

void CppObject::positionUpdated(const QGeoPositionInfo &info)
{
    if (info.isValid())
    {
        if (info.coordinate().isValid())
        {
            emit positionChanged(info.coordinate().latitude(), info.coordinate().longitude());
        }
    }
}

Rien de très compliqué : j'émets mon signal avec les coordonnées physiques à destination des slots connectés. Ensuite vous l'aurez compris, je n'ai plus qu'à créer une instance de mon objet "CppObject" en lui passant une référence sur la frame web parent. Pour cela je vais modifier le constructeur de la classe Html5ApplicationViewerPrivate en y ajoutant mon instantiation d'objet en gras ci-dessous :

Html5ApplicationViewerPrivate::Html5ApplicationViewerPrivate(QWidget *parent)
    : QGraphicsView(parent)
{
    QGraphicsScene *scene = new QGraphicsScene;
    setScene(scene);
    setFrameShape(QFrame::NoFrame);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    m_webView = new QGraphicsWebView;
    m_webView->setAcceptTouchEvents(true);
    m_webView->setAcceptHoverEvents(false);
    setAttribute(Qt::WA_AcceptTouchEvents, true);
    scene->addItem(m_webView);
    scene->setActiveWindow(m_webView);
#ifdef TOUCH_OPTIMIZED_NAVIGATION
    m_controller = new NavigationController(parent, m_webView);
#endif // TOUCH_OPTIMIZED_NAVIGATION
    connect(m_webView->page()->mainFrame(),
            SIGNAL(javaScriptWindowObjectCleared()), SLOT(addToJavaScript()));


    m_shareObject = new CppObject(m_webView->page()->mainFrame());
}

Reste à voir maintenant comment mettre en oeuvre ce que je viens de créer depuis le code html. Le rendu sera le suivant :


Pour cela, je déclare le body comme suit :

<body>
    <a id="quit">X</a>
    <div id="latitude"></div>
    <div id="longitude"></div>
</body>

Et voici la partie entête (<head>) :

<script type="text/javascript">

   function slot(lat, lng)
   {
      var objectString = "SLOT called with Latitude: "+lat+" Longitude: "+lng;
      CppObject.displayTrace(objectString);


      document.getElementById("latitude").innerHTML="Latitude : "+lat;
      document.getElementById("longitude").innerHTML="Longitude : "+lng;
   }

   window.onload = function()
   {
      /* This connects CppObject signal to our slot */
      CppObject.positionChanged.connect(slot);


      /* This calls a slot which then in turn emits the signal. */
      CppObject.startWatchingPosition();


      document.getElementById("quit").onmousedown = function()
      {
          Qt.quit();                    
      };
   }
</script>


Dans la fonction de chargement de la page, je m'abonne au signal "positionChanged" en définissant le gestionnaire "slot" puis je démarre le monitoring de changement de position via le slot public "startWatchingPostion" exposé par l'object C++ CppObjet. Et dans le gestionnaire d'événement j'affiche les nouvelles coordonnées physiques.

Comme d'ahbitude vous trouverez le code source complet ici.

Attention : Bien que le HTML5 soit correctement géré par Qt Webkit, l'implémentation sur Symbian est très light. Je préconise donc de rester en HTML 4 tout en utilisant le wizard HTML5.

vendredi 6 janvier 2012

Géolocalisation en QML/C++ avec points d'intérêts personnalisés

Bonjour,
Dans ce nouvel article, je vais vous exposer un certain nombre de fonctionnalités en Qt/QML autour de la géolocalisation sur Symbian^3 / Anna / Belle :
  1. Afficher une carte ovi centrée sur un lieu donné
  2. Ajouter des points d'intérêts
  3. Interagir avec les points d'intérêts
  4. Récupérer les points d'intérêts depuis un flux XML
  5. Gestion du panning utilisateur
Mon environnement utilisé est le SDK Qt 1.1.4 (Qt 4.7.4 / Qt Mobility 1.2.1) sous Windows 7 et l'application développée a été validée sur Nokia E7 upgradé en Anna.

Le but de l'application est d'afficher sur une carte les stations de vélos sur Paris, avec pour chaque station, le nom et le numéro identifiant. Cet article s'inspire directement d'un wiki disponible sur le site de développeurs nokia. Les données sur les stations sont accessibles via un flux XML public disponible ici : comme vous pouvez le constater, il contient une liste de marqueurs :

<marker name="00901 - STATION MOBILE 1" number="901" address="ALLEE DU BELVEDERE PARIS 19 - 0 75000 Paris -" fullAddress="ALLEE DU BELVEDERE PARIS 19 - 0 75000 Paris - 75000 PARIS" lat="48.892745582406675" lng="2.391255159886939" open="1" bonus="0"/>

Dans notre exemple nous nous intéresserons aux champs suivants :
  • name : nom de la station
  • lat : latitude de la station
  • lng : longitude de la station
Je tiens à rappeler que cet article est à usage uniquement pédagogique pour comprendre le fonctionnement des apis Qt/QML de géolocalisation ; l'application peut donc être à tout moment impactée en cas de maintenance de ce service.

Voici une capture représentant le rendu final :



Le picto "Nokia" représente les bureaux de Nokia France et le rectangle gris affiche le nom et le numéro de la station qui a été cliquée par l'utilisateur.

Pour afficher une carte en QML, il est tentant d'utiliser l'élément QML Map : malheureusement cet élément est très limité notamment dans les versions 1.x de Qt Mobility. Or Qt Mobility 1.2 n'étant pas encore déployée sur les terminaux Symbian Anna, je vous propose ici une méthode compatible avec toutes les versions : l'astuce consiste à définir un élément QML Map en C++. Ce nouvel élément personnalisé sera exposé depuis le C++ vers QML via l'appel classique suivant :

qmlRegisterType<MapWidget>("mywidgets", 1, 0, "MyCustomMap");

La classe C++ implémentant l'objet carte personnalisé se nomme MapWidget : nous aurons l'occasion de la détailler largement par la suite. Depuis un fichier QML, il faudra alors importer de manière classique la version 1.0 du module "mywidgets" :

import mywidgets 1.0

Et dans les faits, l'instantiation de notre carte personnalisée se fera classiquement ainsi :

MyCustomMap {
     id: map
     anchors.fill: parent
     centerLatitude: 48.9018112
     centerLongitude: 2.3748493
     zoomLevel:15
          ...
}
 
Rentrons maintenant dans le vif du sujet ...

Partie C++

J'ai défini une classe dédiée à la gestion de la carte dénommée MapWidget qui dérive directement de la classe QGraphicsGeoMap disponible dans Qt Mobility. Et voici le fichier header :

class MapWidget : public QGraphicsGeoMap
{
    Q_OBJECT

    Q_PROPERTY(int zoomLevel WRITE setCustomZoomLevel)
    Q_PROPERTY(double centerLatitude READ centerLatitude WRITE setCenterLatitude)
    Q_PROPERTY(double centerLongitude READ centerLongitude WRITE setCenterLongitude)
    Q_PROPERTY(bool isQtm12 READ isQtm12)


public:
    MapWidget();
    ~MapWidget();

    void setCustomZoomLevel(int aLevel);
    double centerLatitude() const;
    void setCenterLatitude(double lat);
    double centerLongitude() const;
    void setCenterLongitude(double lon);
    bool isQtm12() const;

    Q_INVOKABLE void addNokiaPoi();
    Q_INVOKABLE void addPoi(double lat, double lon, int uuid);
    Q_INVOKABLE void addPoiGroup();


signals:
    void poiClicked(QString uuid, double posX, double posY);
    void poiHidden();


protected:
    void mousePressEvent(QGraphicsSceneMouseEvent* event);
    void mouseReleaseEvent(QGraphicsSceneMouseEvent* event);
    void mouseMoveEvent(QGraphicsSceneMouseEvent* event);


private:
    static QGeoMappingManager* createManager();
    QPointF lastPos;

    QList<QGeoMapPixmapObject*> myObjectsList;
    QGeoMapGroupObject *poiGroup;
    bool m_isQtm12;
};

Cette classe exposera 4 propriétés au monde QML :
  • zoomLevel : le niveau de zoom : c'est une valeur décimale (qreal) dont le range dépend de la plateforme. Sur Symbian il va de 0.0 à 20.0. Il existe des bornes définies sous la forme de constante (minimumZoomLevel et maximumZoomLevel)   
  • centerLatitude : la latitude du centre de la carte
  • centerLongitude : la longitude du centre de la carte
  • isQtm12 : booléen indiquant si Qt Mobility 1.2.x est installée sur le smartphone (nécessaire pour contourner une régression présente depuis la version 1.2)
La classe s'accompagne donc des méthodes de lecture/écriture associées à ces propriétés :

// Indique si la version courante de Qt Mobility est 1.2.x
bool MapWidget::isQtm12() const
{
    return m_isQtm12;
}

// Méthode publique permettant d'ajuster le zoom depuis QML
void MapWidget::setCustomZoomLevel(int aLevel)
{
    setZoomLevel(aLevel);
}

// Méthode publique pour récupérer la latitude du point surlequel est centrée la carte
double MapWidget::centerLatitude() const
{
    return center().latitude();
}

// Méthode publique pour récupérer la longitude du point surlequel est centrée la carte
double MapWidget::centerLongitude() const
{
    return center().longitude();
}

// Méthode publique pour modifier la latitude du point surlequel est centrée la carte
void MapWidget::setCenterLatitude(double lat)
{
    QGeoCoordinate c = center();
    c.setLatitude(lat);
    setCenter(c);
}

// Méthode publique pour modifier la longitude du point surlequel est centrée la carte
void MapWidget::setCenterLongitude(double lon)
{
    QGeoCoordinate c = center();
    c.setLongitude(lon);
    setCenter(c);
}

Le constructeur du widget, outre la détection de version de Qt Mobility, fait appel au constructeur parent QGraphicsGeoMap en lui passant une instance QGeoMappingManager crée par une méthode privée de type singleton "createManager" :

MapWidget::MapWidget() :
    QGraphicsGeoMap(createManager())
{
    m_isQtm12 = detectQTMVersion();

    setCenter(QGeoCoordinate(48.89, 2.37));
    setZoomLevel(7);

    if(m_isQtm12) poiGroup = new QGeoMapGroupObject();
}

La méthode createManager instancie le plugin de géolocalisation "nokia" et renvoie l'instance de QGeoMappingManager associée à l'instance du plugin.

QGeoMappingManager* MapWidget::createManager()
{
    /*QMap<QString, QVariant> parameters;
    parameters["app_id"] = "APPID";
    parameters["token"] = "TOKEN";*/


    // Utilisation du plugin de localisation nokia
    QGeoServiceProvider* serviceProvider = new QGeoServiceProvider("nokia" /*, parameters*/);


    // Récupération du gestionnaire graphique de carte pour gérer les intéractions
    QGeoMappingManager* mappingManager = serviceProvider->mappingManager();


    return mappingManager;
}

Note : depuis peu, il est nécessaire de s'enregistrer pour obtenir un jeton (token) afin d'utiliser ce plugin de géolocalisation

Ensuite elle déclare trois méthodes invocables depuis QML :
  •  addNokiaPoi : pour ajouter sur la carte le point d'intérêt correspondant aux bureaux Nokia France (picto Nokia sur le screenshot)
void MapWidget::addNokiaPoi()
{
    QGeoCoordinate coord(48.9018112, 2.3748493);
    QPixmap pixmap = QPixmap(":qml/CustomQmlMap/nokia.png");
    QGeoMapPixmapObject *pixMapObject = new QGeoMapPixmapObject(coord, QPoint(-16,-20), pixmap);
    addMapObject(pixMapObject);
}
  • addPoi : pour ajouter une station de vélo sur la carte. Une station sera vue comme une triplette : longitude / latitude / identifiant. A noter que ce nouveau POI sera mémorisé dans une liste (myObjectsList) en plus d'être ajouté à la carte (ou au groupe de POIs en version QTM 1.2)
void MapWidget::addPoi(double lat, double lon, int uuid)
{
    // Création du point d'intérêt
    QGeoCoordinate coord(lat, lon);
    QPixmap pixmap = QPixmap(":qml/CustomQmlMap/poi.png");
    QGeoMapPixmapObject *pixMapObject = new QGeoMapPixmapObject(coord, QPoint(-16,-20), pixmap);
    pixMapObject->setObjectName("poiMarker");

    QVariant v(uuid);
    pixMapObject->setProperty("uuid", v);

    myObjectsList.append(pixMapObject);

    if(m_isQtm12)
    {
        // Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
        // https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
        poiGroup->addChildObject(pixMapObject);
    }
    else
        addMapObject(pixMapObject);
}

Note : les ressources bitmaps utilisées sont référencées dans un fichier de ressource "images.qrc" dont le chemin relatif est "qml/QmlCustomMap"
  • addPoiGroup : pour ajouter un groupement de points d'intérêts sur la carte. Cette méthode est uniquement utilisée pour contourner un bug QTM 1.2
void MapWidget::addPoiGroup()
{
    // Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
    // https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
    if(poiGroup && m_isQtm12) addMapObject(poiGroup);
}

Cette classe dispose par ailleurs de deux signaux récupérables au niveau QML :
  • poiClicked : indiquant que l'utilisateur a cliquer un point d'intérêt particulier. Côté QML cela permettra de dessiner un rectangle gris contenant le nom de la station
  • poiHidden : déclenché lorsque l'utilisateur se déplace dans la carte. Côté QML cela permettra de masquer tout rectangle gris précédemment affiché.
Ensuite j'ai surchargé trois gestionnaires d'événements (mousePressEvent, mouseReleaseEvent et mouseMoveEvent) afin de gérer les interactions utilisateurs avec la carte (principalement le clic sur POI et le pan) :

  • mousePressEvent : appelé lorsque l'utilisateur touche l'écran ; j'y mémorise la position (X, Y) dans une propriété de la classe ; cette propriété sera utilisée  dans le gestionnaire mouseReleaseEvent afin de déterminer quel POI a été cliqué.
 // Mémorisation de la position (cette valeur sera ensuite utilisée dans la méthode mouseReleaseEvent)
void MapWidget::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    lastPos = event->pos();
}
  • mouseReleaseEvent : appelé lorsque l'utilisateur retire son doigt de l'écran ; le but de cette méthode est de récupérer les informations du POI que l'utilisateur a ciblé afin de les remonter au niveau QML via l'émission du signal poiClicked :
void MapWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
    QPointF releasePos = event->pos();
    QPointF diff = lastPos - releasePos;

    if (qAbs(diff.x()) > 30 || qAbs(diff.y()) > 30) {
        return;
    }

    int selectedIndex = -1;
    double minDiffX = -1;
    double minDiffY = -1;


    if(myObjectsList.length()>0)
    {
        // Nous parcourrons la liste des POIs
        for(int i=0;i<myObjectsList.length();i++)
        {
            QGeoMapPixmapObject* obj = myObjectsList[i];

            QPointF currentPoiPos = coordinateToScreenPosition(obj->coordinate());
            QPointF invalid(0,0);


            if(currentPoiPos != invalid)
            {
                diff = lastPos - currentPoiPos;

                double currentDiffX = qAbs(diff.x());
                double currentDiffY = qAbs(diff.y());

                // Nous mémorisons l'index du POI le plus proche du doigt de l'utilisateur
                if(selectedIndex == -1)
                {
                    selectedIndex = i;
                    minDiffX = currentDiffX;
                    minDiffY = currentDiffY;
                }
                else
                {
                    if(minDiffX>=currentDiffX && minDiffY>=currentDiffY)
                    {
                        selectedIndex = i;
                        minDiffX = currentDiffX;
                        minDiffY = currentDiffY;
                    }
                }
            }
        }

        if(selectedIndex != -1)
        {
            // Nous récupérons les propriétés du marker correspondant (uuid, position X, position Y) et émettons un signal
            QGeoMapPixmapObject* obj = myObjectsList[selectedIndex];
            if (obj->objectName() == "poiMarker")
            {
                QString uuid = obj->property("uuid").toString();
                qDebug() << "Emit poiClicked signal with id : " << uuid;
                emit poiClicked(uuid, releasePos.x(), releasePos.y());
            }
        }

    }
}

  • mouseMouveEvent : appelé lorsque l'utilisateur glisse son doigt sur l'écran ; cette méthode a deux objectifs : déplacer visuellement la carte via l'appel à QGraphicsGeoMap::pan  et émettre le signal poiHidden afin de masquer le détail du POI courant qui pourrait potentiellement être affiché dans un rectangle gris en overlay côté QML.
void MapWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
    QPoint lastPos = event->lastPos().toPoint();
    QPoint pos = event->pos().toPoint();

    int dx = lastPos.x() - pos.x();
    int dy = lastPos.y() - pos.y();

    pan(dx, dy);

    emit poiHidden();
}

Enfin j'ai défini quelques propriétés privées déjà plus au moins décrites plus haut :
  • lastPos : position (X, Y) correspondant à l'endroit où l'utilisateur a toucher l'écran. Cette variable est positionné dans le gestionnaire mousePressEvent
  • myObjectsList : contient la liste des POIs (objet de type QGeoMapPixmapObject) ajoutés sur la carte
  • poiGroup : groupement de POIs (objet de type QGeoMapGroupObject) contenant tous les POIs. Utilisé avec QTM 1.2 seulement. 
Note : pour QTM 1.2 nous ajoutons tous les POIs dans un groupe afin d'ajouter ce groupe à la carte en un seul appel (addPoiGroup) ce qui déclenche un seul "redraw" de la carte. Depuis QTM 1.2, le fait d'ajouter un POI directement dans la carte déclenche un "redraw"  complet de la carte (et non de la zone du POI) : comme il y a 1200 POIs, les performances deviennent catastrophiques d'où l'ajout dans un groupe (ce qui ne déclenche pas de "redraw") qui n'est ajouté à la carte qu'une fois.

Partie QML

Maintenant que vous avez une bonne idée de l'implémentation de notre widget de cartographie personnalisé, voyons comment l'utiliser depuis QML. Ayant crée le projet avec le wizard "Application Qt Quick", nous nous retrouvons avec deux fichiers QML : main.qml et MainPage.qml. N'ayant pas modifier le fichier "main.qml" je ne décrirai que le second fichier MainPage.qml.

Commençons par les règles d'importation :

import QtQuick 1.1
import QtMobility.location 1.2
import com.nokia.symbian 1.1    // Symbian components
import mywidgets 1.0


Pensez à référencer le module mywidgets afin d'utiliser notre élément personnalisé QmlCustomMap.
La page principale contient tout d'abord une partie pour gérer l'indicateur de chargement :

Page {
    id: rootWindow
    width: 360
    height: 640

    // Loading screen
    Rectangle {
        id:loadingScreen
        anchors.fill: parent
        opacity: 0.7
        color: "white"
        visible: true

        property alias loadingTxt: bgText.text

        state: "showLoadingScreen"
        states: [
            State {
                name: "showLoadingScreen"
                PropertyChanges {
                    target: loadingScreen
                    visible:true
                    z:1
                }
            },
            State {
                name: "hideLoadingScreen"
                PropertyChanges {
                    target: loadingScreen
                    visible:false
                    z:0
                }
            }
        ]

        Text{
            id: bgText
            anchors.centerIn: parent
            color: "black"
            text: "Chargement de la carte"
        }

        BusyIndicator {
            id: indicator
            width:40
            height:40
            anchors.verticalCenter: parent.verticalCenter
            anchors.right: bgText.left
            anchors.rightMargin: 20
            running: true
        }
    }


Plus important : la création du model de données récupérés initialisée avec un flux XML :
   
XmlListModel {
    id: stations
    source: http://www.velib.paris.fr/service/carto
    query: "/carto/markers/marker"
    XmlRole {name: "number"; query: "@number/string()"}
    XmlRole {name: "name"; query: "@name/string()"}
    XmlRole {name: "lat"; query: "@lat/string()"}
    XmlRole {name: "lng"; query: "@lng/string()"}

    onStatusChanged: {
        if(status == XmlListModel.Ready) {
            console.log("Model Status: ready")
            var i = 0
            for(i=0;i<stations.count;i++)
            {
                map.addPoi(stations.get(i).lat, stations.get(i).lng, parseInt(stations.get(i).number));
            }
            console.log("No more POIs to add")


            // Utilisation d'un groupe de POIs uniquement pour une version 1.2.x de Qtmobility (bug 1664)
            // https://bugreports.qt.nokia.com//browse/QTMOBILITY-1664
            if(map.isQtm12) map.addPoiGroup()


            // Ajout du bureau Nokia à Paris
            map.addNokiaPoi()

                        // On masque la mire de chargement

            loadingScreen.state = "hideLoadingScreen"
            loadingScreen.loadingTxt = ""
       }
       else if(status == XmlListModel.Error) {
            console.log("Model Status: error")
       }
       else if(status == XmlListModel.Loading) {
            console.log("Model Status: loading")
            loadingScreen.loadingTxt = "Chargement des POIs"
       }
    }
}

Le parsing des données XML est un parsing de type XPath (voir documentation pour plus d'info). C'est très simple à utiliser. Dans notre cas je récupère pour chaque "marker" (i.e. POI) les valeurs de balise "number", "name", "lat" et "lng". A réception des données (c'est à dire quand le status passe à l'état "XmlListModel.Ready"), j'ajoute chacun des POIs sur la carte.

Maintenant je vais créer une instance de l'élément personnalisé QmlCustomMap centré sur les bureaux de Nokia France avec un zoom élevé:

MyCustomMap {
    id: map
    anchors.fill: parent
    centerLatitude: 48.9018112
    centerLongitude: 2.3748493
    zoomLevel:15

    onPoiClicked: {
        var i = 0
        for(i=0;i<stations.count;i++)
        {
            if(stations.get(i).number == uuid)
            {
                poiRect.x = posX-85
                poiRect.y = posY-44
                poiRect.poiTxt = stations.get(i).name
                poiRect.visible = true
                break;
            }
        }
    }

    onPoiHidden: {
        poiRect.visible = false
    }

    Rectangle {

        property alias poiTxt: poiTextElement.text

        id: poiRect
        visible:false
        width:170
        height:75
        color: "darkgrey"
        radius:10
        opacity: 0.95

        Text {
            id:poiTextElement
            color:"white"
            font.bold: true
            font.pointSize: 6
            anchors.centerIn: parent
            text:""
            width:parent.width
            wrapMode:Text.WordWrap
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
                if(poiRect.visible) poiRect.visible = false
            }
        }
   }
}

Le rectangle identifié par "poiRect" est masqué par défaut et affiché uniquement lorsque l'utilisateur clique sur un POI : il consiste en un rectangle gris arrondi contenant l'identifiant et le nom de la station. Vous pouvez tout à fait utiliser une image à la place. Je vais donc jouer sur la propriété "visible" de cet élément : pour cela je mets en place les gestionnaires de signaux onPoiClicked et onPoiHidden.

Et voilà c'est fini : à titre d'exercice je vous invite à afficher plus d'information sur un POI donné en utilisant le second service XML :  http://www.velib.paris.fr/service/stationdetails/ + number

Le code source complet est disponible ici