Here is another proof that Qt is not as ubiqutious as I would like it to be. When I ask around what developers use to develop desktop apps using web technologies (HTML, JavaScript, CSS), the choice is either Adobe AIR or Titanium Desktop. Seems that a lot of people overlook Qt and its built-in WebKit integration.
Rather than ranting, I will start a bunch of blog posts which shows how to bring rich web apps into the desktop. This will be the first now. For branding purposes, we will call it hybrid approach because this literally brings web technologies and native solution in the same plate.
I have talked about this hybrid stuff before at MeeGo Conf 2011 (see the video) and will do it again for Intel Elements 2011. However, for that matter, I think blog posts still serve as a better written reference. It also gives me a chance to clarify some prejudices, confusions, and myths that some people still have about web technologies.
For this post, I’ll show you how to implement something like this screenshot:
It’s a code editor (supporting JavaScript mode) based on CodeMirror, an excellent web-based editing widget written using JavaScript and DOM. There are other similar projects out there, for this example I found out that CodeMirror is the easiest to work with. Note that we will use CodeMirror2, a complete rewrite which is way better and faster than the old version of CodeMirror.
The entire code is available in the usual X2 git repo (or the alternative repo), look under webkit/codemirror
(you need Qt 4.6 or later versions). It essentially contains of two major classes, Editor
which is the main window representing the application and CodeMirror
which is the wrapper around CodeMirror’s JavaScript implementation. The latter is just a subclass of QWebView where we host CodeMirror actual logic.
Because we do not want to run CodeMirror from the file system and we want the editor to work even without Internet connection, we use the technique of storing all the necessary content in the resource system provided by Qt. This is what assets.qrc
all about, i.e. as a central location to find the following:
index.html codemirror.js codemirror.css javascript.js default.css cobalt.css elegant.css neat.css night.css
Many still mistakenly believe that using web technologies means that the app requires constant network connection. This is not necessarily anymore with the said packaging approach. It is important to note that now we can access any of the files stored inside the resource using the special qrc
scheme. This is exactly what the constructor is doing, i.e. loading qrc:/index.html
right into the web view:
CodeMirror::CodeMirror(QWidget *parent)
: QWebView(parent)
{
load(QUrl("qrc:/index.html"));
changeTheme("default");
m_external = new External(this);
page()->mainFrame()->addToJavaScriptWindowObject("External", m_external);
}
The main application class, Editor
, has all the important code to setup native menu (we have three main menu items: File, Edit, Theme) and file dialog if the user opens a new file or save the text. Of course, when we save to a file, we need to get the text out of our hosted CodeMirror and write it using QFile. This is where we work with bridging the two different worlds: native and web. There is a whole section about QtWebKit bridge in the Qt documentation.
Looking at index.html
, you would find that the CodeMirror’s editor object is created as (surprise!) editor
. Thus, in order to get the content we have to call editor.getValue
(see CodeMirror manual for the details), hence effectively transferring the data from the web world to the native world:
QString CodeMirror::text() const
{
return page()->mainFrame()->evaluateJavaScript("editor.getValue()").toString();
}
What about the other way around? We could simply evaluate a JavaScript code that sets the editor’s value, e.g. editor.setValue(content)
. Rather than passing content as a string (and thus we need to escape it properly), let’s use another nice feature of QtWebKit bridge: built-in integration with QObject. We setup a simple QObject subclass called as External
whose purpose is only to hold a string property:
class External: public QObject
{
Q_OBJECT
Q_PROPERTY(QString data READ data)
public:
External(QObject *parent = ): QObject(parent) {}
QString text;
QString data() const { return text; }
};
An instance of this class is inserted into our web page during the construction of CodeMirror class (see the previous code fragment), under the name External
. From now on, anytime in the web world we call window.External.data
, we would get the same exact value as what we put into the data
property of our instance. It is doing what we want to do: transferring the data from the native world to the web world.
This seems to be complicated but check out at how simple the function finally looks like:
void CodeMirror::setText(const QString &text)
{
m_external->text = text;
page()->mainFrame()->evaluateJavaScript("editor.setValue(External.data)");
}
Arguably, the special function evaluateJavaScript is really critical to this hybrid approach! In addition to assist the bridging, it is also useful to trigger some action in the web world from the native world:
void CodeMirror::undo()
{
page()->mainFrame()->evaluateJavaScript("editor.undo()");
}
void CodeMirror::redo()
{
page()->mainFrame()->evaluateJavaScript("editor.redo()");
}
This function is also necessary for the theming support. All the styles needed for the different themes are available from the resource (the last few CSS files in the previous list), but we need to activate it (bonus exercise, why the last line dealing with class name is necessary?):
void CodeMirror::changeTheme(const QString &theme)
{
QWebFrame *frame = page()->mainFrame();
frame->evaluateJavaScript(QString("editor.setOption('theme', '%1')").arg(theme));
frame->evaluateJavaScript(QString("document.body.className = 'cm-s-%1'").arg(theme));
}
Of course, since we use Qt, the same editor example runs beautifully on other platforms (also here showing different color schemes available for CodeMirror):
With a pretty thin wrapper (sloccount gives around 250 lines), we bring CodeMirror to the desktop and build a simple editor around it, with menu, file, editing, and theme support. Of course, credits should go to the brave WebKit team, Qt folks, and CodeMirror developers, whose hard work is leveraged in this demo.
Obviously, being an example, this hybrid editor still lacks a lot of standard features and hence can’t be really used as a killer application. However, if someone wants to pursue the path of productized hybrid app, don’t hesitate to use it as a starting basis.
This short demo highlights just two points. First, we can package the web content using the resource system, thus it does not need to reside as individual files on the disk or even a remote server. Second, transferring the data between the web world and the native world is possible by leveraging the bridge provided by QtWebKit.
In the coming installments, hopefully we can explore more about the bridging mechanism. Of course, feel free to propose a specific topic or concern that you want me to tackle first!