QtWebkit Javascript Bridge

QtWebkit bridge allows the Javascript environment in a QWebView to call python code through the Qt slots mechanism.  This allows a webpage to interact with the Qt application.

The basic setup is quite easy – select a QObject to be exposed to javascript and call the addToJavaScriptWindowObject() method of a QWebView’s main frame. This exposes any slots on the QObject to javascript and makes them callable.  Given that all Qt classes derive from QObject, you can easily allow javascript access to interface elements by passing a QWidget to the bridge.  Alternatively, you can create a QObject subclass and write a custom API by defining your methods as slots – this is what I’ve shown below.

There are a few tricks to know about how types are converted back and forward between javascript and python and you’ll want to be sure that your API persists between webpages which adds a few lines of code.  So let’s take a look at the code –

# tell PyQt to automatically convert between python strings and QString
import sip
sip.setapi('QString', 2)

from PyQt4.QtCore import QObject, QUrl, pyqtSlot
from PyQt4.QtWebKit import QWebView, QWebSettings

# turn on developer tools in webkit so we can get at the javascript console for debugging
QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True)<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1">​</span>

The first thing we do is set the internal QString API to version 2.  What this does is tells PyQt to automatically convert between python str and QString types, saving us the effort!

Line 15 is a directive to Webkit to enable developer tools, allowing us to access the javascript console.

# any QObject can be added to the javascript window object
# slots are then callable from javascript
class PythonAPI(QObject):

    # take two integers and return an integer
    @pyqtSlot(int, int, result=int)
    def add(self, a, b):
        return a + b

    # take a list of strings and return a string
    # because of setapi line above, we get a list of python strings as input
    @pyqtSlot('QStringList', result=str)
    def concat(self, strlist):
        return ''.join(strlist)

    # take a javascript object and return string
    # javascript objects come into python as dictionaries
    # functions are represented by an empty dictionary
    @pyqtSlot('QVariantMap', result=str)
    def json_encode(self, jsobj):
        # import is here to keep it separate from 'required' import
        return json.dumps(jsobj)

    # take a string and return an object (which is represented in python
    # by a dictionary
    @pyqtSlot(str, result='QVariantMap')
    def json_decode(self, jsstr):
        return json.loads(jsstr)

Now we get to the API itself.  In this example, I’ve created four slots on the QObject which will be callable from javascript.

The first slot is defined as @pyqtSlot(int, int, result=int) which tells the bridge to expose the add() function and to cast the received parameters to integers.  We’re also telling the bridge that we’ll be returning an integer to javascript.

For all basic types, the setup is the same.  The remainging three slots show how the bridge handles converting some more complex types.

Line 31 defines a slot which accepts a QStringList.  As you might expect, this tells Qt that we’re expecting to receive an array of strings as the parameter to the javascript function call which is converted into a python list of strings before calling the python function. For the case where you are looking to pass an array of differently typed objects, you can use QVariantList.

The remaining two slots (lines 38 and 45 respectively) deal with converting from and to javascript objects – the first of which accepts an object as a parameter, the second, returning an object.  Javascript objects are represented in python with dictionaries.  It’s important to note that javascript object methods are translated to python as empty dictionaries.

# client webview which will have QObject js->python bridge
class Client(QWebView):
    def __init__(self):
        super(Client, self).__init__()

        # create python api object
        self.api = PythonAPI()

        # get the main frame of the view so that we can load the api each time
        # the window object is cleared
        self.frame = self.page().mainFrame()
        self.frame.javaScriptWindowObjectCleared.connect(self.load_api)

    # event handler for javascript window object being cleared
    def load_api(self):
        # add pyapi to javascript window object
        # slots can be accessed in either of the following ways -
        #   1.  var obj = window.pyapi.json_decode(json);
        #   2.  var obj = pyapi.json_decode(json)
        self.frame.addToJavaScriptWindowObject('pyapi', self.api)

Now we create a custom QWebView subclass just to handle exposing the API consistently.  All we’re doing here is instantiating our API and setting up an event handler to add the API to the frame.  This is  important because the javascript window object is cleared everytime the page is refreshed or changed, or you call setHtml() on the view.  Subscribing to the QFrame.javaScriptWindowObjectCleared event allows us to re-add the API each time this occurs.

if __name__ == '__main__':
    import sys
    from PyQt4.QtGui import QApplication

    app = QApplication(sys.argv)

    view = Client()
    view.setUrl(QUrl('test.html'))
    view.show()

    sys.exit(app.exec_())

The remaining code sets up the Qt application, creates and instance of the custom QWebView and sets the url to our test page.

Right-click on the QWebView window and select Inspect to bring up the Webkit developer tools.  Navigate to the javascript console for the output from the test file.

You’ll notice that you can also directly interact with the API in the console as per the image below.

One of the best uses of the javascript bridge is making it possible to use Javascript libraries in Qt applications. The immediate application that comes to mind is Google Maps.

/**
 * client.py
 * QWebkit javascript bridge example
 * author - tim
 */
 
# tell PyQt to automatically convert between python strings and QString
import sip
sip.setapi('QString', 2)

from PyQt4.QtCore import QObject, QUrl, pyqtSlot
from PyQt4.QtWebKit import QWebView, QWebSettings

# turn on developer tools in webkit so we can get at the javascript console for debugging
QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True)

# not required, used for example javascript->python calls
import json

# any QObject can be added to the javascript window object
# slots are then callable from javascript
class PythonAPI(QObject):

    # take two integers and return an integer
    @pyqtSlot(int, int, result=int)
    def add(self, a, b):
        return a + b

    # take a list of strings and return a string
    # because of setapi line above, we get a list of python strings as input
    @pyqtSlot('QStringList', result=str)
    def concat(self, strlist):
        return ''.join(strlist)

    # take a javascript object and return string
    # javascript objects come into python as dictionaries
    # functions are represented by an empty dictionary
    @pyqtSlot('QVariantMap', result=str)
    def json_encode(self, jsobj):
        # import is here to keep it separate from 'required' import
        return json.dumps(jsobj)

    # take a string and return an object (which is represented in python
    # by a dictionary
    @pyqtSlot(str, result='QVariantMap')
    def json_decode(self, jsstr):
        return json.loads(jsstr)


# client webview which will have QObject js->python bridge
class Client(QWebView):
    def __init__(self):
        super(Client, self).__init__()

        # create python api object
        self.api = PythonAPI()

        # get the main frame of the view so that we can load the api each time
        # the window object is cleared
        self.frame = self.page().mainFrame()
        self.frame.javaScriptWindowObjectCleared.connect(self.load_api)

    # event handler for javascript window object being cleared
    def load_api(self):
        # add pyapi to javascript window object
        # slots can be accessed in either of the following ways -
        #   1.  var obj = window.pyapi.json_decode(json);
        #   2.  var obj = pyapi.json_decode(json)
        self.frame.addToJavaScriptWindowObject('pyapi', self.api)


if __name__ == '__main__':
    import sys
    from PyQt4.QtGui import QApplication

    app = QApplication(sys.argv)

    view = Client()
    view.setUrl(QUrl('test.html'))
    view.show()

    sys.exit(app.exec_())
<!--
    test.html
    QWebkit javascript bridge example
    author - tim
 -->
        
<html>
    <script>
        function do_stuff() {
            console.info(pyapi.add(1, 2));

            var strlist = ['a', 'b', 'c'];
            console.info(pyapi.concat(strlist));

            var jsobj = {
                a: 1,
                b: 'test',
                c: 1.23
            };
            var json = pyapi.json_encode(jsobj);
            console.info(json);

            console.info(pyapi.json_decode(json));
        }
    </script>
    <body>
        <a href="javascript:do_stuff()">Click Me!</a>
    </body>
</html>

Leave a Reply

Your email address will not be published. Required fields are marked *