So far in this tutorial we've been mainly aiming at desktop use of the application. If you attempted to build it as is for a mobile platform you would quite quickly notice a number of issues that prevent it from being really usable, and we will now address these one by one.
Most mobile platforms have multiple network connections, which are not connected all the time, and change regularly in operation. Qt provides the Bearer API for managing these and the events that occur with them.
At present when our application starts it simply assumes that a network link is available and that a mapping plugin will load. This could quite easily not be the case, so we will use the Bearer API to verify the state of the network and ensure that it is running before we call initialize().
First up, we need a QNetworkConfigurationManager in order to get at the default configuration of our environment:
class MainWindow : public QMainWindow { ... private: QNetworkConfigurationManager *netConfigManager; };
We create it in the constructor of MainWindow. As mentioned in the documentation for QNetworkConfigurationManager, we should make a call to updateConfigurations() before actually making use of the instance. So we'll also need a slot to be called when this completes (we name this openNetworkSession()).
MainWindow::MainWindow() : ... { ... netConfigManager = new QNetworkConfigurationManager; connect(netConfigManager, SIGNAL(updateCompleted()), this, SLOT(openNetworkSession())); netConfigManager->updateConfigurations(); }
And in the slot itself we use the defaultConfiguration() method as the parameter to construct a new QNetworkSession to represent our default connection to the network.
We first check to see if this session is open, and if so, call initialize() right away. Otherwise, we hook up an appropriate signal and wait for the network to be available.
void MainWindow::openNetworkSession() { session = new QNetworkSession(netConfigManager->defaultConfiguration()); if (session->isOpen()) { initialize(); } else { connect(session, SIGNAL(opened()), this, SLOT(initialize())); session->open(); } }
So now our initialize() method will be called once a network connection is available.
In our current implementation we depend upon the presence of a mouse wheel in order to zoom in and out on the map. This is not terribly useful in environments that lack a mouse (ie, anything except a desktop or laptop computer). To address this, we will implement a simple pair of zoom buttons on the right-hand side of the map display.
We also currently assume that panning the map using a mouse or touch screen is possible, which is not the case on, for example, many Symbian devices, which lack touch input. To rectify this, we will add support for handling arrow key events in GeoMap.
First up, our zoom buttons. We're going to use a very similar setup to that which we used for the sliding status bar previously, and create a new subclass of QGraphicsRectItem:
class ZoomButtonItemPrivate; class ZoomButtonItem : public QGraphicsRectItem { public: explicit ZoomButtonItem(GeoMap *map); void setRect(qreal x, qreal y, qreal w, qreal h); private: ZoomButtonItemPrivate *d; bool isTopHalf(const QPointF &point); bool isBottomHalf(const QPointF &point); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event); void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); };
Our button is going to simply be a translucent rectangle, with the top half containing a "+" symbol, which zooms in when clicked, and the bottom half containing a "-" symbol, which zooms out. In the constructor we create the two text items:
class ZoomButtonItemPrivate { public: GeoMap *map; QGraphicsSimpleTextItem *plusText; QGraphicsSimpleTextItem *minusText; bool pressedOverTopHalf; bool pressedOverBottomHalf; }; ZoomButtonItem::ZoomButtonItem(GeoMap *map) : d(new ZoomButtonItemPrivate) { d->map = map; d->pressedOverBottomHalf = false; d->pressedOverTopHalf = false; setPen(QPen(QBrush(), 0)); setBrush(QBrush(QColor(0,0,0,150))); d->plusText = new QGraphicsSimpleTextItem(this); d->plusText->setText("+"); d->plusText->setBrush(QBrush(Qt::white)); d->minusText = new QGraphicsSimpleTextItem(this); d->minusText->setText("-"); d->minusText->setBrush(QBrush(Qt::white)); }
And in setRect() we manage sizing and aligning the text items so that they each occupy roughly half the space.
void ZoomButtonItem::setRect(qreal x, qreal y, qreal w, qreal h) { QGraphicsRectItem::setRect(x, y, w, h); QFont f; f.setFixedPitch(true); f.setPixelSize(h/3.0); d->plusText->setFont(f); d->minusText->setFont(f); QRectF plusBound = d->plusText->boundingRect(); QPointF plusCenter(x+w/2.0, y+h/4.0); QPointF plusDelta = plusCenter - plusBound.center(); d->plusText->setPos(plusDelta); QRectF minusBound = d->minusText->boundingRect(); QPointF minusCenter(x+w/2.0, y+3.0*h/4.0); QPointF minusDelta = minusCenter - minusBound.center(); d->minusText->setPos(minusDelta); }
Finally, we use the boolean flags in ZoomButtonItemPrivate, above, to manage click detection in the mousePressEvent and mouseReleaseEvent functions:
void ZoomButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { const QPointF pos = event->pos(); if (!d->pressedOverTopHalf && !d->pressedOverBottomHalf) { if (isTopHalf(pos)) { d->pressedOverTopHalf = true; } else if (isBottomHalf(pos)) { d->pressedOverBottomHalf = true; } } event->accept(); } void ZoomButtonItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { const QPointF pos = event->pos(); if (isTopHalf(pos) && d->pressedOverTopHalf) { d->map->setZoomLevel(d->map->zoomLevel() + 1.0); } else if (isBottomHalf(pos) && d->pressedOverBottomHalf) { d->map->setZoomLevel(d->map->zoomLevel() - 1.0); } d->pressedOverBottomHalf = false; d->pressedOverTopHalf = false; event->accept(); }
In this way, if the mouse (or finger for touch screens) is pressed and then released over the same half of the ZoomButtonItem, we perform the zoom action appropriately. We could have simply hooked the corresponding events on the children items, plusText and minusText, but as they occupy less space and their size/shape vary depending on the default font, users may find it difficult to target the active portion of the button (especially in a touch environment).
Adding the new button item to the MapsWidget also happens similarly to before:
void MapsWidget::initialize(QGeoMappingManager *manager) { ... d->zoomButtonItem = new ZoomButtonItem(d->map); sc->addItem(d->zoomButtonItem); resizeEvent(0); ... } void MapsWidget::resizeEvent(QResizeEvent *event) { if (d->view && d->map) { ... d->zoomButtonItem->setRect(width()-30, height()/2.0 - 35, 25, 70); } }
And now we can zoom in and out properly on touch devices. Next we'll address the need to pan and zoom on devices with neither touch nor mouse, which we can do through handling key events.
To do this we override the keyPressEvent() method on GeoMap:
void GeoMap::keyPressEvent(QKeyEvent *event) { QGeoCoordinate center; QPropertyAnimation *anim; const qreal width = size().width(); const qreal height = size().height(); switch (event->key()) { case Qt::Key_4: case Qt::Key_Left: center = screenPositionToCoordinate( QPointF(width/2 - width/5, height/2)); anim = new QPropertyAnimation(this, "centerLongitude"); anim->setEndValue(center.longitude()); anim->setDuration(200); anim->start(QAbstractAnimation::DeleteWhenStopped); break; case Qt::Key_6: case Qt::Key_Right: center = screenPositionToCoordinate( QPointF(width/2 + width/5, height/2)); anim = new QPropertyAnimation(this, "centerLongitude"); anim->setEndValue(center.longitude()); anim->setDuration(200); anim->start(QAbstractAnimation::DeleteWhenStopped); break; case Qt::Key_2: case Qt::Key_Up: center = screenPositionToCoordinate( QPointF(width/2, height/2 - height/5)); anim = new QPropertyAnimation(this, "centerLatitude"); anim->setEndValue(center.latitude()); anim->setDuration(200); anim->start(QAbstractAnimation::DeleteWhenStopped); break; case Qt::Key_8: case Qt::Key_Down: center = screenPositionToCoordinate( QPointF(width/2, height/2 + height/5)); anim = new QPropertyAnimation(this, "centerLatitude"); anim->setEndValue(center.latitude()); anim->setDuration(200); anim->start(QAbstractAnimation::DeleteWhenStopped); break; case Qt::Key_1: if (zoomLevel() > minimumZoomLevel()) { setZoomLevel(zoomLevel() - 1); } break; case Qt::Key_3: if (zoomLevel() < maximumZoomLevel()) { setZoomLevel(zoomLevel() + 1); } break; } this->setFocus(); event->accept(); }
We allow both the arrow keys (which map to the sides of the D-pad on some devices), and the numbers 2, 8, 6 and 4 to pan the map, which some users may find more comfortable.
In addition, the 1 and 3 keys allow zooming in and out. This key mapping is very similar to that used by the majority of maps applications on Symbian, and should be familiar to most users.
In summary, in this tutorial we have built a simple maps and navigation application from scratch using the Qt Location API.
We first built the basic maps widget, then added a UI and search capability, followed by basic routing and some tuning for use on mobile platforms.
The full code as at the end of the tutorial is available in the Qt Mobility examples, named "mapsdemo".