Embedding a Leaflet map in a Qt application

Posted on

Embedding a map in a Qt application using Leaflet is surprisingly easy. The QtWebKit.QWebView object can be used to create a browser window within a Qt dialog. Within this browser window you can create an interactive map using Leaflet just as you would for any other browser. Communication between Qt and the Leaflet map is also possible, allowing you to control the map from the main application, or extract information from Leaflet.

This post demonstrates a simple application written in Python using PyQt4, which loads a large map in the main dialog and reports the coordinates (latitude and longitude) of the centre of the map in a QLabel.

Leaflet JS within PyQt4

Contains Ordnance Survey data © Crown copyright and database right 2014

If you’ve not done so already I recommend following the Leaflet quick start guide in order to become familiar with using Leaflet in a regular browser window.

The HTML document used in our application is the bare minimum required to load the Leaflet library and create a placeholder for the map.

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
    <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
    <style>
        body { padding: 0; margin: 0; }
        html, body, #map { height: 100%; }
    </style>
</head>
<body>
    <div id="map"></div>
</body>
</html>

The JavaScript is also fairly standard, with the exception of the MainWindow object which is explained later.

A new map object is created and focused on London. A Mapbox tile layer is added (which requires an API key), and a marker with popup is placed on the centre of the map. The last few lines bind a function to the event triggered when the map is moved, calling MainWindow.onMapMove with the coordinates of the map’s new centre point. Note that this JavaScript is not included in the HTML document, but instead will be inserted by Qt later.

access_token = 'pk.eyJ1Ijoic25vcmZhbG9ycGFndXMiLCJhIjoic1oxRTMxcyJ9.eC6cjtLmFs9EcVwmT1isOQ';
var map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tiles.mapbox.com/v4/examples.map-i86nkdio/{z}/{x}/{y}@2x.png?access_token='+access_token, {
    maxZoom: 18,
    attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
        '<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
        'Imagery &copy; <a href="http://mapbox.com">Mapbox</a>',
    id: 'examples.map-i86nkdio',
}).addTo(map);

var marker = L.marker(map.getCenter()).addTo(map);
marker.bindPopup("Hello World!").openPopup();

if(typeof MainWindow != 'undefined') {
    var onMapMove = function() { MainWindow.onMapMove(map.getCenter().lat, map.getCenter().lng) };
    map.on('move', onMapMove);
    onMapMove();
}

The Python application has been kept as simple as possible. A QWidget is created with a QLabel, QWebView and QPushButton contained within a vertical layout.

The QWebView.load method is called to load map.html. When the document has loaded the QWebView.loadFinished signal is emitted, which calls the MainWindow.onLoadFinished method. This method reads the JavaScript from map.js and evaluates it in the browser window using the evaluateJavaScript method.

If the button is pressed the MainWindow.panMap method is called, which evalutes some JavaScript in the browser window causing the map to pan to Paris. This demonstrates passing information from Python to JavaScript. If the JavaScript statement were to return something it would be returned by the evaluateJavaScript method as a QVariant, thereby passing information from JavaScript back to Python.

Calling Python methods from JavaScript is also possible, but requires a little more effort. A MainWindow object is made available to JS by setupUi using the addToJavaScriptWindowObject method. The first argument to this function is the name of the object as it will be seen in JS, while the second argument is the object to make available (in this case the instance of MainWindow). To make a method in Python available from JS the @QtCore.pyqtSlot decorator must be used.

An example of this is given by the MainWindow.onMapMove method. A method is created in JS to call onMapMove with the current centre coordinates of the map, which is then bound to the 'move' event of the Leaflet map, so that when the map pans the coordinates displayed in the QLabel are updated.

#!/usr/bin/env python

from PyQt4 import QtCore, QtGui, QtWebKit
import functools

class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi()
        self.show()
        self.raise_()
    
    def setupUi(self):
        self.setFixedSize(800, 500)
        
        vbox = QtGui.QVBoxLayout()
        self.setLayout(vbox)
        
        label = self.label = QtGui.QLabel()
        vbox.addWidget(label)

        view = self.view = QtWebKit.QWebView()
        view.page().mainFrame().addToJavaScriptWindowObject("MainWindow", self)
        view.load(QtCore.QUrl('map.html'))
        view.loadFinished.connect(self.onLoadFinished)
        vbox.addWidget(view)
        
        button = QtGui.QPushButton('Go to Paris')
        panToParis = functools.partial(self.panMap, 2.3272, 48.8620)
        button.clicked.connect(panToParis)
        vbox.addWidget(button)
    
    def onLoadFinished(self):
        with open('map.js', 'r') as f:
            frame = self.view.page().mainFrame()
            frame.evaluateJavaScript(f.read())

    @QtCore.pyqtSlot(float, float)
    def onMapMove(self, lat, lng):
        self.label.setText('Lng: {:.5f}, Lat: {:.5f}'.format(lng, lat))
    
    def panMap(self, lng, lat):
        frame = self.view.page().mainFrame()
        frame.evaluateJavaScript('map.panTo(L.latLng({}, {}));'.format(lat, lng))

if __name__ == '__main__':
    app = QtGui.QApplication([])
    w = MainWindow()
    app.exec_()

So far I’ve not been able to get geolocation working, although it should be supported according to this answer on StackOverflow. If anyone can get this to work post a comment!

Leaflet is a simple but powerful library and there is a lot of potential to do some cool things by integrating it with existing Qt applications. If you have an example of this let me know and I might link to it here.