Pretty pictures
Example running as a stand alone Qt/Webkit application
Same application in FireFox running as an Apache module
Downloads
Disk Space Analyzer Demo Application
Source Code
Introduction
HTML5
is a great way to create rich, platform-independent
User Interfaces.
The freedom and flexibility HTML5 offers Web Developers has
eluded most native UI tools. The UI interface that can be
hammered out fairly quickly and easily in HTML5 can be a
longer less productive experience with other C++ GUI tools.
In this article, I'm going to attempt to help bridge the
gap between HTML5 and C++ so that we can use HTML5 to create
the UI portion of stand alone C++ applications. Two key
elements I will use to make this happen are
WebKit and Qt.
A KHTML forked
browser engine that is very solid and easy to compile and use.
It is the engine behind
Chrome,
Safari,
Android and
iPhone browsers.
A platform-independent native application framework developed by Trolltech
and recently acquired by Nokia. It is very reliable, complete and free to use.
Qt has what is probably the easiest to use WebKit integration, and our
framework surrounding it is going to be very minimal. In fact, my hope is that
after studying it, you will feel that you could move the frame work from
Qt+WebKit to say, wxWidgets+Mozilla,
or MFC+IEActiveX with little trouble. You'll only feel this
way of course, until you try to embed Mozilla ;)
Motivation
Some reasons you might want to use HTML5 for UI development:
- Web tools are probably the fastest, most cost effective
and well understood method of creating UI's currently available.
There are tons of free libraries, example code and support available.
- Reuse website resources, designs (and designers),
HTML/JavaScript code, layouts, images, etc.
- Web based UI's are highly platform-independent. This app will also build
as an Apache module or IIS CGI module. I plan to add an Android example soon.
- An embedded web server could be used to allow remote access to a server,
or provide the interface for a service that cannot directly access the users
desktop.
- Perhaps your application needs to couple tightly to a web site or online database.
- It's not as far out there as you might think, other companies such as
Netflix
are using this approach to UI development.
If you were to search the web for examples and tutorials of WebKit based applications,
you would find many. However, most will demonstrate techniques like executing JavaScript
functions directly from C++ and making a lot of calls into the engine itself.
Having developed many applications using embedded web engines, I'm convinced that this
is not the route to take. It will end up being an obstacle to scalability as more and
more of internals of the engine are exposed to the higher application logic.
The C++ code will become specialized around the implementation of the
browser engine as will the HTML/JavaScript code. Many of the potential benefits of
using the web engine will be quickly lost.
To get the full benefit, we want to be able to use C++ code and libraries we find as is.
We also want to be able to use HTML/JavaScript code and libraries we find such
as jQuery or Prototype as is. So, we will need a clean break between our
HTML/JavaScript/C++ and the Browser Engine. This will also be the key to maintaining
compatibility with platforms other than Webkit/Qt.
As it turns out, things are already designed this way, and all we have to do is make
sure we don't break the rules in our integration. We will only interface our C++ with
the UI using normal page requests. This will help avoid many common pitfalls, and we
won't have to go digging around in the WebKit engine.
Goals
I'm going to describe a method to integrate an embedded web browser
(specifically WebKit) with C++ code in an efficient and scalable way. I'm even going
to take it another step and supply a basic and flexible code base to get you
started. I'm going to make the assumption that you have some experience with Web
Development and with C++. I'm not going into great detail on how to use C++ or HTML5 -
I'm going to focus on integrating the two.
So let's lay down some objectives.
- Create a platform-independent framework for HTML5 application development
leveraging Qt and WebKit.
- Since we want to be platform independent, ensure everything builds with
GCC and Cygwin.
But don't worry, there's no reason things shouldn't work with Visual Studio as well.
- Provide an example, keeping the HTML/JavaScript and C++ seperate and pure as possible.
Taming C++
To make interfacing C++ easier, I'm borrowing a lesson from scripting languages
and embedding the C++ code directly into HTML documents. This allows native expression
for HTML/JavaScript code and makes it easy for C++ to present output data. As long as
embedded C++ code is kept succinct, it will be easy to debug and there will be more
upside than downside. We will be able to write code like the following:
<html>
<?global
int add( int a, int b )
{
return a + b;
}
?>
<body>
5 + 7 = <?c out << add( 5, 7 ) ?>
<ul>
<?c
for ( int i = 0; i < 10; i++ )
{
?>
<li><?c out << i ?></li>
<?c
}
?>
</body>
</html>
This means that web developers and designers without C++ knowledge will be able to largely
understand these documents and work with them to modify JavaScript code, apply themes and
adjust CSS values, using their own prefered methods and tools even though they will not be
fully functional until compiled.
I am introducing two special tags into the document: <?global ?> and
<?c ?>.
<?global ?>
Whatever appears in the <?global ?> tag, will be inserted at the top of the
final CPP file (the global space). This tag can contain #include statements and function
definitions that will be scoped to the CPP file. <?global ?> tags can appear anywhere,
they will all be extracted and inserted in the order they appear at the top of the generated
CPP document.
<?c ?>
The contents of the <?c ?> tag will go into an internal function in the order they
appear. The function is called each time the page is to be rendered. The prototype for this
internal function looks like this in my implementation:
int _internal_run( TPropertyBag< str::t_string8 > &in, TPropertyBag< str::t_string8 > &out );
This is of course something you could change to your liking. In my implementation, the two
TPropertyBag parameters provide all the data io. The in variable contains GET, POST
and other request data. The out variable defines the response that is returned to the client.
For the most part, it behaves like std::basic_string<>. In fact, it's actually a wrapper for
this string object, but it also allows any manner of multi-dimensional response. Ultimately it ends up
being a plain vanilla C++ file, and you can do anything here that you would in any other C++ file.
In order to make these documents come to life, we will have to turn the code
"inside-out" so that it can be built using a standard C++ compiler. I have done
this by creating a precompiler that processes the code before the C++ compiler. It turns out
that the precompiler is simple to write and the extra complexity of a precompiler more than pays
for itself in making the C++/Web interface neat and seamless.
Code like this:
<html>
<?global
#include <string>
?>
<b> <?c out << std::basic_string< char >( "Hello" ) + " World" ?> </b>
</html>
Is turned into this and compiled:
#include <string>
static int _internal_run( TPropertyBag< str::t_string8 > &in,
TPropertyBag< str::t_string8 > &out )
{
out << "<html>\n\n";
out << "\n\t<b>\n\t\t";
out << std::basic_string< char >( "Hello" ) + " World" ;
out << "\n\t</b>\n</html>";
return 0;
}
Later when the function is called, it will write this into the out variable:
<html>
<b> Hello World </b>
</html>
We then pass this output to WebKit for processing. The HTML/Javascript will simply
link to the pages as though they contained normal HTML and be oblivious about the
embedded C++ code. If you're already a PHP developer, you should feel right at home at
this point, except for eerie feelings about how the frame work will know how to
map the file name to a function pointer. Funny you should ask...
Links to all the compiled functions are stored in a C++ array and referenced by
the filename. We can then easily lookup the appropriate function to call based on
the filename request we get from the WebKit engine. I'm even rooting the site at the
host name embedded, so that we can still allow the WebKit engine to transparently
access data from the Internet should we choose to allow that.
As a bonus, the precompiler also embeds our other resources such as images, JavaScript
and basically any other files we want into the application in a platform-independent way.
So, we end up with a single exe or dll that contains our entire User Interface!
One thing to be aware of. When there are C++ errors, the compiler will reference
the precompiled file instead of the original source. Though it should still be clear
to you what's going on, for this reason you will probably want to keep most
the heavy lifing in standard C++ files and reserve the embedding for interfacing only.
Qt
As for the Qt work, it will be minimal. As I mentioned earlier, we will not need
to delve into the depths of WebKit. I can easily outline the main parts of the integration.
Create the main window
Disable things such as scroll bars and context menus (or don't, if you prefer) to make
things more 'application-like'.
CMainWindow::CMainWindow()
{
m_pView = new QWebView( this );
m_pNet = new CNetworkMgr( this, m_pView->page()
->networkAccessManager() );
m_pView->page()->setNetworkAccessManager( m_pNet );
m_pView->page()
->mainFrame()
->setScrollBarPolicy( Qt::Vertical,Qt::ScrollBarAlwaysOff );
m_pView->page()
->mainFrame()
->setScrollBarPolicy( Qt::Horizontal,Qt::ScrollBarAlwaysOff );
m_pView->setContextMenuPolicy( Qt::PreventContextMenu );
m_pView->settings()
->setAttribute( QWebSettings::XSSAuditingEnabled, 0 );
m_pView->settings()
->setAttribute( QWebSettings::LocalContentCanAccessRemoteUrls,
1 );
m_pView->load( QUrl( "http://embedded/index.htm" ) );
}
Overload QNetworkMgr and QNetworkReply to broker page requests
Here we will intercept page requests and redirect to our embedded resources and
C++ embedded code.
QNetworkReply* CNetworkMgr::createRequest( QNetworkAccessManager::Operation op,
const QNetworkRequest &req, QIODevice *device )
{
if ( req.url().host() == "embedded" )
return new CNetworkReply( this, req, op );
return QNetworkAccessManager::createRequest( op, req, device );
}
CNetworkReply::CNetworkReply( QObject *parent,
const QNetworkRequest &req,
const QNetworkAccessManager::Operation op )
: QNetworkReply( parent )
{
QByteArray path = req.url().path().toUtf8();
HMRES hRes = res.FindResource( 0, full.c_str() );
str::t_string mime = res.GetMimeType( hRes );
if ( isBinaryResource( hRes ) )
{
const void *ptr = res.Ptr( hRes );
unsigned long sz = res.Size( hRes );
m_content.append( QByteArray::fromRawData( (const char* )ptr, sz ) );
}
else
{
mime = "text/html";
CHmResources::t_fn pFn = res.Fn( hRes );
if ( pFn )
{
TPropertyBag< str::t_string8 > in;
TPropertyBag< str::t_string8 > out;
pFn( in, out );
m_content.append( out.data(), out.length() );
}
} }
That's the general idea - the rest is implementation details. You can
see the framework bleed is minimal, mainly the in and out TPropertyBag
variables. This could have been replaced with a STDOUT capture (more like standard
CGI). I decided not to do this in this implementation since I like to use STDOUT for
debugging.
I've provided make files for bash/gcc that should work for you out of the
box on Linux or Windows using Cygwin. For instructions on installing Cygwin on Windows
see htmapp wiki.
Wrapping up
I've provided a sample app that I hope you'll agree looks pretty good, it's yet another
disk analyzer. I was able to leverage open source packages like Quicksand and d3 to get it
done in a lot less time.
To make the development a bit easier, you might consider building the application as a
CGI or Apache module during development so that you can use standard browser tools to debug
the 'live' build, though the lack of persistence may still render it a bit incomplete. In
the example I provide for instance, the background thread does not run as an CGI module.
Running the application with an embedded web server would solve this issue. I hope to add
this feature soon.
If you want to look further, I have another library, that does
have an embedded web server along with support for Visual Studio and Android, as well as
many other features. If you're interested, you can take a look at that
here.
Thanks for reading, I hope you found this article somewhat enlightening.
CREDITS
My most gracious thanks to the following projects: