Wrapping Webkit (Part 3 - Qt Quick/Python)
This time I'm going to stick with Qt but use its new shiny: Qt Quick. Qt Quick adds a markup language, QML, to the mix.
You use QML to declare which components to build your user interface from. The idea is that a declarative specification will make your user interface work across different types of device (mobile is the main focus).
Let's start with the QML for our example. It needs to do a couple of things:
- Declare a WebView (Webkit) component which will load our HTML page.
have a couple of methods to do the following:
- Return data read from standard input by our Python code.
- Ask our Python code to exit the application.
First we import QtQuick and QtWebKit:
import QtQuick 1.0 import QtWebKit 1.0
Next we declare the WebView component. This will be the only Qt component in our application:
Notice a couple of things here:
- bridge is just a proxy to another object called the_bridge. the_bridge is undefined in our QML; our Python code will have to define it and add to our WebView's runtime environment. Ideally I'd have exposed the_bridge directly to the page but I couldn't get that to work.
Our Python code has to do what the C++ code did in my previous example:
Handle command line arguments specifying:
- The URL of a page to load into Webkit.
- Run as a fullscreen application.
- Hide the mouse cursor.
- Enable Webkit's developer tools.
Read data from standard input and make it available to the page when done.
Expose a function to the page so it can exit the application.
First up we need to import a bunch of modules:
#!/usr/bin/env python import sys import argparse import signal from os import path from threading import Lock from PySide import QtGui, QtDeclarative, QtCore from PySide.QtWebKit import QWebSettings
You can see the Qt modules we're going to use. QtDeclarative handles the QML stuff. Notice also that I'm using the standard Python Lock class rather than QMutex. Either would have worked fine.
Parsing command line arguments
Python's already got decent support for parsing command line arguments via the argparse module. So it's pretty straightforward for us:
parser = argparse.ArgumentParser(description='Webkit Example') parser.add_argument('-u', '--url', help='page to load', default='file://' + sys.path + '/test.html') parser.add_argument('-f', '--fullscreen', help='run in fullscreen mode', action='store_true') parser.add_argument('-c', '--hidecursor', help='hide mouse cursor', action='store_true') parser.add_argument('-d', '--debug', help='enable web inspector', action='store_true') args = parser.parse_args()
The default page to load is test.html in the same directory as the application. The other arguments default to False (argparse assumes this because the action to take when they're specified is store_true).
We also have to pass the command line through to QApplication when we initialise Qt:
app = QtGui.QApplication(sys.argv)
Next we'll define a class which will read from standard input and raise a Qt signal with the data when it's done:
class DataReader(QtCore.QObject): def __init__(self): super(DataReader, self).__init__() @QtCore.Slot(str) def read(self): self.readsig.emit(sys.stdin.read()) readsig = QtCore.Signal(str)
As you can see, it's pretty simple to declare slots and signals in Python: use the Slot decorator and the Signal constructor. One thing to note is signals have to declared as class attributes. However, Qt makes sure each instance of your class has a separate runtime signal object.
Now it's time to define a class which will be exposed to QML (it'll implement the the_bridge object we left undefined above):
class Bridge(QtCore.QObject): def __init__(self): super(Bridge, self).__init__() self.data = '' self.lock = Lock()
data will contain data read from standard input by DataReader when it's done. We also create a mutex (Lock) so we can safely read and write to data from multiple threads...
@QtCore.Slot(result=str) def getData(self): with self.lock: return self.data @QtCore.Slot(str) def gotData(self, data): with self.lock: self.data = data
We have to declare getData as a slot so it can be called from QML. gotData is a slot so we can hook it up to a DataReader later on.
Finally, we define a signal to raise when application exit is required, plus a function (slot) to raise it:
exitsig = QtCore.Signal(str) @QtCore.Slot(str) def exit(self, msg): self.exitsig.emit(msg)
Creating a view
Now we need to create a QDeclarativeView, which is like a Qt window but uses a QML file to build the user interface:
view = QtDeclarative.QDeclarativeView() view.setWindowTitle('WebKit Example') view.setResizeMode(QtDeclarative.QDeclarativeView.ResizeMode.SizeRootObjectToView)
We set the window title here and tell it to resize its contents when it is resized. Note we don't load the QML into it yet — we have some more setting up to do first.
Hooking it all up
First let's make a DataReader and a Bridge:
reader = DataReader() bridge = Bridge()
Now we need to let bridge know when reader finishes reading from standard input:
When bridge raises an exit signal, we want to exit the application by closing view:
def exit(msg): print msg view.close() bridge.exitsig.connect(exit)
Then we can start reader on a separate thread:
readerThread = QtCore.QThread() readerThread.started.connect(reader.read) reader.readsig.connect(readerThread.quit) reader.moveToThread(readerThread) readerThread.start()
There's three odds and ends we need to take care of:
Enable Webkit's developer tools if specified on the command line:
Hide the mouse cursor if specified on the command line:
if args.hidecursor: app.setOverrideCursor(QtGui.QCursor(QtCore.Qt.BlankCursor))
Note this current generates X errors on Ubuntu 12.10 64-bit due to a bug in PySide.
Enable Ctrl-C to terminate the program (PySide seems to disable it):
Showing the view
Now we can close things out by loading our QML and showing it.
First we need to add bridge to view's runtime environment, making it available to QML as the_bridge:
Next we load the QML — I place it in a file alongside the Python source:
Now the QML is loaded, the Webkit component is available as the top-level (root) object in view. So we can load a page into it:
Finally, we show view on the screen (in fullscreen mode if specified on the command line):
if args.fullscreen: view.showFullScreen() else: view.show()
And start Qt's message loop etc:
Test Web page
To test our example, we can use exactly the same Web page we used to test our C++ version:
Test it by piping data to webkit-example.py:
echo 'Hello World!' | ./webkit-example.py
You can find all the source from this article here.
blog comments powered by Disqus