Wrapping Webkit (Part 2 - Qt/C++)
In my previous post, I showed how to get bi-directional communication going between Javascript and Vala, using GTK+'s Webkit component.
This time I'm going to do the same between Javascript and C++, using Qt's Webkit component. Last time out I decided against using C++ with GTK+ but Qt seems better suited to the language. Let's see how Qt/C++ compares to GTK+/Vala.
We'll be sticking to the same example we used before with Vala:
- A main function for parsing command line options and initializing things.
- A window class which embeds Webkit and puts it in a Qt window.
Main function
The first thing to do in a Qt application is declare a QApplication object:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
This initializes Qt so it's a good idea to do it early on.
We'll support the same command line options: url, fullscreen, hidecursor and debug. Qt doesn't have a helper class to parse command line options so we'll use TCLAP, a nice small library for doing just that:
TCLAP::CmdLine cmd("Webkit Example");
TCLAP::ValueArg<std::string> urlArg("u", "url", "page to load", false, "file://" + QDir(a.applicationDirPath()).filePath("test.html").toStdString(), "URL", cmd);
TCLAP::SwitchArg fullscreenSwitch("f", "fullscreen", "run in fullscreen mode", cmd, false);
TCLAP::SwitchArg hidecursorSwitch("c", "hidecursor", "hide mouse cursor", cmd, false);
TCLAP::SwitchArg debugSwitch("d", "debug", "enable web inspector", cmd, false);
cmd.parse(argc, argv);
As you can see, we specify the default value for each option at the same time. url defaults to a file called test.html in the same directory as the application.
Now we create our Qt window which embeds Webkit (see the next section for details of our window class):
MainWindow w(debugSwitch.getValue());
Make it full screen if the command line option was passed:
if (fullscreenSwitch.getValue())
{
w.setWindowState(Qt::WindowFullScreen);
}
A slight difference with the Vala version is that we hide the mouse cursor using the application object rather than the window:
if (hidecursorSwitch.getValue())
{
a.setOverrideCursor(QCursor(Qt::BlankCursor));
}
Next we can load the URL into Webkit (this is a method on our window class which ends up calling into Webkit):
w.load(urlArg.getValue().c_str());
and show the window and all its children (including Webkit):
w.show():
Finally, we have to start the Qt application (i.e. the main event loop):
return a.exec();
Window class
Declaration
This class is going to do the following:
- Inherit from the Qt QMainWindow class so it's a top-level window.
- Apply settings and add widgets defined visually in Qt Creator, Qt's IDE. In our GTK+/Vala example, we did this ourselves in code. With Qt Creator, you can configure things like the window's size and add a Webkit component to it visually using a form designer.
- Specify what Webkit features are enabled.
- Start a thread which reads data from standard input.
- Expose an object to Javascript which has two methods:
- A method which returns data read by the thread from standard input. This shows we can get data from C++ into the Web app.
- A method which writes its argument to standard output and then terminates the application. This shows we can call C++ functions and pass them data from the Web app.
Here's how we declare our window class:
class MainWindow : public QMainWindow
{
Q_OBJECT
You can see we inherit from QMainWindow. We also have to use the Q_OBJECT macro in our class because we'll be using Qt signals and slots. Signals and slots are declared like any other C++ method but Qt can connect a signal to a slot at runtime. When the signal method is called, Qt makes sure that any slot methods connected to it are also called. We'll be using signals slots in this example.
Next we declare our constructor and destructor:
public:
explicit MainWindow(bool debug, QWidget *parent = 0);
~MainWindow() {}
and a public method to load a URL into Webkit:
void load(const char *url);
The MainWindow class has the following private data:
private:
Ui::MainWindow ui;
DataReader reader;
QThread readerThread;
Bridge bridge;
Ui::MainWindow is a class which Qt Creator's form designer generates from your visual design for the window. Qt Creator saves your design as an XML file which is then converted into this class. You can find the XML file for this example here. I put a grid layout onto the window and then dragged a QWebView widget onto the layout.
DataReader is a class we'll define later which reads data from standard input and raises a signal with the data when it's done. This will be done in a thread (readerThread).
Bridge is also a class we'll define later. It contains the methods we want to expose to Javascript: one to retrieve the data read by reader from standard input and one to exit the application. It should also have a slot which can receive the data from reader and store it.
Finally, we can define a couple of slots — we'll connect them to signals later:
private slots:
void addBridgeToPage();
void exit(QString msg);
addBridgeToPage will be called whenever a new page is loaded into Webkit. It will add bridge to the page. exit will print its argument to standard output and then close the window. Note we declare these slots private. This just means the methods which implement them are private to the class — Qt can connect the slots themselves to signals in any class.
Implementation
MainWindow's implementation is pretty simple. Let's look at the constructor first:
MainWindow::MainWindow(bool debug, QWidget *parent) :
QMainWindow(parent)
{
QWebSettings::globalSettings()->setAttribute(QWebSettings::PluginsEnabled, true);
QWebSettings::globalSettings()->setAttribute(QWebSettings::JavascriptEnabled, true);
QWebSettings::globalSettings()->setAttribute(QWebSettings::LinksIncludedInFocusChain, false);
QWebSettings::globalSettings()->setAttribute(QWebSettings::LocalContentCanAccessRemoteUrls, true);
QWebSettings::globalSettings()->setAttribute(QWebSettings::LocalStorageEnabled, true);
if (debug)
{
QWebSettings::globalSettings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
}
You can see we set a bunch of Webkit options:
- Enable plugins (you usually don't need this).
- Enable Javascript.
- Enable tabbing between links.
- Allow pages loaded from local disk to make calls to remote URLs. The Javascript in our example doesn't do this but it's useful if you want to distribute a HTML/JS user interface and have it communicate with a server somewhere.
- Enable Local (DOM) storage. Again, our example doesn't actually need to do this.
- Enable Webkit's Web inspector if the debug parameter is true.
Next we have to initialize the user interface we designed visually using Qt Creator's form designer:
ui.setupUi(this);
Now we need to hook up a bunch of signals and slots. First, add bridge to the global Javascript environment when a page is loaded. We do this by connecting the javaScriptWindowObjectCleared signal from Webkit to our addBridgeToPage method (which we'll define later):
connect(ui.webView->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(addBridgeToPage()));
Next, when Javascript raises the exit signal in bridge, arrange for the exit method in MainWindow to be called:
connect(&bridge, SIGNAL(exit(QString)), this, SLOT(exit(QString)));
When reader has finished reading data from standard input, notify bridge so it can store the data for Javascript to receive when it polls for it:
connect(&reader, SIGNAL(dataRead(QString)), &bridge, SLOT(gotData(QString)));
Finally, we need to arrange for reader to be run in a separate thread so it doesn't block the main user interface:
connect(&readerThread, SIGNAL(started()), &reader, SLOT(read()));
connect(&reader, SIGNAL(dataRead(QString)), &readerThread, SLOT(quit()));
reader.moveToThread(&readerThread);
readerThread.start();
The recommended approach to starting a thread in Qt uses signals and slots, as you can see above. You connect the started signal to the slot that will do the work. Then once the work is done (dataRead), tell the thread to stop (quit). Before starting the thread, you must set the affinity of the object which will be doing the work (moveToThread).
Now we can define MainWindow's methods: load, addBridgeToPage and exit.
void MainWindow::load(const char *url)
{
//ui->webView->load(QUrl(url));
ui.webView->setHtml("<script>location.replace('" + QString(url) + "');</script>");
}
load tells the Webkit component (webView) to visit a URL. If you use Webkit's load method to do this, you get an extra entry in the history. You can see above I use an alternative which runs some Javascript to replace the current page instead.
void MainWindow::addBridgeToPage()
{
ui.webView->page()->mainFrame()->addToJavaScriptWindowObject("bridge", &bridge);
}
addBridgeToPage is called whenever a new page is loaded. It adds bridge to the page so Javascript can call it.
void MainWindow::exit(QString msg)
{
QTextStream(stdout) << msg << endl;
close();
}
Remember we connected MainWindow::exit to the exit signal raised by bridge (this signal is raised when Javascript calls the exit method on bridge after we exposed it to the page).
DataReader class
Declaration
This class just has to read data from standard input and raise a signal with the data when it's done:
class DataReader : public QObject
{
Q_OBJECT
private slots:
void read();
signals:
void dataRead(QString data);
};
Implementation
We only need to implement read — Qt takes care of generating a method for raising the dataRead signal (the method has the same prototype as the signal but you have to use the emit keyword when calling it from C++):
void DataReader::read()
{
emit dataRead(QTextStream(stdin).readAll());
}
Bridge class
Declaration
An object of this class (bridge in MainWindow) will be exposed to Javascript. It has:
- A signal, exit. Javascript apps can just call the exit method on the Bridge object to raise the signal. Remember we connected this signal to the exit method in MainWindow.
- A slot, getData, which can be called from Javascript to retrieve data read from standard input. If no data has yet been read, it should return an empty string.
- A slot, gotData, which will receive data read from standard input and store it so it can be returned to Javascript when it calls getData.
Here's what this looks like in code:
class Bridge : public QObject
{
Q_OBJECT
signals:
void exit(QString msg);
public slots:
QString getData();
// Override slot inherited from QObject which shouldn't be exposed!
// See https://bugs.webkit.org/show_bug.cgi?id=34809
void deleteLater() {}
private slots:
void gotData(QString data);
Finally, we need a member variable to store the data and a mutex because Javascript may be calling getData at the same time that gotData is being called (I'm unclear as to where Javascript calls are handled so it's best to be safe):
private:
QMutex mutex;
QString data;
};
Implementation
getData and gotData are really simple: they just get and set data inside a lock on mutex:
QString Bridge::getData()
{
QMutexLocker locker(&mutex);
return data;
}
void Bridge::gotData(QString data)
{
QMutexLocker locker(&mutex);
this->data = data;
}
Test Web page
To test our example, we can re-use the Web page we used to test our Vala version, with a simple modification to call call exit and getData via bridge rather than as separate functions:
<html>
<head>
<script type="text/javascript">
function check_data()
{
var data = bridge.getData();
if (data === "")
{
setTimeout(check_data, 1000);
}
else
{
document.getElementById('data').innerText = data;
}
}
</script>
</head>
<body onload='check_data()'>
<p>
data: <span id="data"></span>
</p>
<input type="button" value="Exit" onclick="bridge.exit('goodbye from Javascript')">
</body>
</html>
Test it, as before, by piping data to webkit-example:
echo 'Hello World!' | ./webkit-example
You can find all the source from this article here.
blog comments powered by Disqus