Native / desktop applications with SmartClient

I had these days an interesting conversation with friend of mine, about how to migrate web applications back to the desktop respectively if it is possible to have web programmers develop native desktop applications using the knowledge they already have and some other topics around these ideas. (While Qt offers a way to develop native applications using javascript and declarative code, this is not what we’re looking for, but instead a way to reuse the knowledge the web programmers already have in various frameworks like Ext-JS, SmartClient, JQuery, angular, etc. in a native (desktop) environment).

Deploying web applications as native applications was already done for a while by using WebKit or some other browser component embedded in the desktop application, used to render the user interface of the application in same way as the browsers would do. Assuming the browser component you’re using is up to the HTML standards, should be no issue at all and everything should be up and running in no time, right? Well, yes but .. there are couple of things to consider, depending on what exactly your in-browser application’s native counterpart is going to do. If you want just to hide the browser itself and present an installable application to the end user, then the framework’s code and the application’s code can be downloaded from your server, just as it would in case of web browser. The installed application would be minimal, just small cosmetic difference, with preset URLs, hidden from the end-user.
If however you want something different, like running the entire application locally and just connect remotely just to data providers / database (via rest services for example), or you want to run your application entirely local, you start running out of options quite soon. The main issue is not running the web application – that is solved by the web component rendering it – but interfacing with the rest of the world and the desktop respectively in order to access remote services or files and local services and files respectively. Since we’re using a web browser component which inherently connects to the network to retrieve data, one would think running a web server locally when application starts, is a must – in order to serve the scripts, assets and process the issued request. Of course, it still requires some sort ‘server side’ mechanism for accessing local services and files. The server then is shut down when the application exits. Then there is also the issue of deployment on the target computer: How the application (frameworks, application code, resources etc) will be installed, updated if required and so on. Having our own server will greatly improve this, as then we’re free to deploy the files compressed, encrypted or whatever is required. But still .. we need to maintain two applications, the embedded web server and the web application. There is however an another approach, which beside the fact that is a bit more friendly (for the developer), it has some other advantages. Enter Qt and it’s QWebView component.

Yes, I know is just a WebKit component, but it also:

  • allows adding new protocols which can be easily hooked back in to application. This is great, because now we don’t need to embed/run an entire web server to handle requests or serve files. We just catch the requests and serve back the response. Which can be whatever form we wish, as long as it is handled by our web application.
  • allow direct integration of native objects into webpage – QObjects can be directly accessed from within the webpage, using javascript. Want to access native side of the application from javascript? Make a QObject, set it up properly and insert it in the page. And that’s it .. you can access it from javascript, you can even call it’s native methods from javascript.Want to render some native components inside the already running web application? Create it natively and just insert it in the page.

In this article (and probably the following ones) I’ll present a way to integrate Qt with SmartClient, which will allow deploying an application which has been written in javascript with the SmartClientframework and as ‘server side’ uses native C++ code, without running an embedded web server. In this first article we’ll write a simple ‘hello world’ application as a proof of concept, and we’ll start adding on more complex things like passing data from native side to DataSources, processing DataSource requests for CRUD operations, accessing native services, DMI calls, etc. While this series of articles is written with SmartClient in mind, nothing stops one to use what’s contained here and apply it to some other web framework(s).

For start, we need to decide how to package the framework and the application sources. For this case I’ve decided to work with Qt Creator and package the SmartClient as a zip file – file which will contain all the framework resources and code. The application’s own javascript files, images, styles, etc will be embedded as resources during compile time.

So let’s fire up Qt Creator and create a simple Qt application. Once that is complete, open the main window’s UI for editing and add a QWebView (we’ll name it webView).
Next, we need a way to differentiate between various kind of requests: framework files need to be served from zip file, application files need to be served from resource, files from local file system should be served differently, network requests should go over network, and so on. Since network requests already use http:/ and resources use qrc:/ I decided to follow the direction and for the framework files (which are stored in a compressed zip file) we’ll use myzip://.

First, we need to inject in the webView the required HTML to load our application which should load the framework and our application. For this we’ll use webView’s page() method to get the page and then page’s frame() method to get the frame:

QWebFrame* frame = ui->webView->page()->mainFrame();

once we have the frame, we build the required HTML code:

QStringList scripts;
    scripts << "isomorphic/system/modules/ISC_Core.js";
    scripts << "isomorphic/system/modules/ISC_Foundation.js";
    scripts << "isomorphic/system/modules/ISC_Containers.js";
    scripts << "isomorphic/system/modules/ISC_Grids.js";
    scripts << "isomorphic/system/modules/ISC_Forms.js";
    scripts << "isomorphic/system/modules/ISC_DataBinding.js";
    scripts << "isomorphic/skins/Enterprise/load_skin.js";

    scripts << "qrc:/js/Application.js";

    QString html = ""\
            ""\
               ""; foreach (QString str, scripts) { html+="\n"; } html+= "" \ "" \ "";

Note that we put the scripts in a QStringList then looped over and built the HTML. Same would be accomplished by directly writing the HTML, but once we start adding new files to the application, is way easier (friendlier) to change the list’s components than editing the required HTML. You can also note that for framework’s files we’ve used relative URLs but for our application’s files we used qrc:/ as we discussed earlier. This however implies that all relative URLs will be served from the zip file as that will be the base URL

Once we have the html, we need to inject it in the frame. For this we use setHtml, which also can take a base URL as parameter. For this parameter we’ll pass myzip:/// as discussed earlier.

	frame->setHtml(html,QUrl("myzip:///"));

Building the application and running it we’ll notice an odd thing: doesn’t do anything however does not display any error either – although is quite clear files cannot be loaded as we haven’t added yet the required code for serving from zip files. To fix this we’ll need to subclass QWebPage and override it’s javaScriptConsoleMessage() and inside that method to display the console messages:

	class MyWebPage : public QWebPage {

	    Q_OBJECT

	..... // constructor declation ..
	    protected:
	        void javaScriptConsoleMessage( const QString & message, int lineNumber, const QString & sourceID );
	.... // some more code ....
	}

and add it’s implementation:

	void MyWebPage::javaScriptConsoleMessage( const QString & message, int lineNumber, const QString & sourceID ) {

	    qDebug() << sourceID << ":" << lineNumber << ":" << message;
	}

Now we need to add this to our web view. We do this in MainWindow’s constructor, just after the UI was set up and before the HTML content was injected in the page:

	ui->webView->setPage(new MyWebPage());

Running the application, we’ll see console messages displayed on application output.

Next, we need to serve the files from the compressed zip. For handling zip file handling I decided to use QuaZip. In order to intercept protocol requests, we need to subclass and create a new QNetworkAccessManager in which will provide the required handling. For this however we’ll also need to subclass QNetworkReply to create our own reply. So, let’s start with this first.

Our reply will need to queue up either a sequence of strings (in case of HTML, javascript, JSON, XML,etc) or a sequence of bytes (in case of images for example). We’ll store this in a QByteArray. We’ll also need to store the original request and how many bytes the browser already read from the response itself.

	class MyNetworkReply : public QNetworkReply {

	    Q_OBJECT

	    public:
	        explicit MyNetworkReply(QNetworkRequest *request, QObject *parent = 0);

	        virtual qint64 readData(char *data, qint64 maxlen);
	        virtual void abort() {}
	        qint64 bytesAvailable() const;
	        bool isSequential() const;

	        void appendContent(QString str);
	        void appendContent(QByteArray ccontent);
	        void finishReply();

	    private:
	        QByteArray content;
	        qint64 offset;
	        QNetworkRequest* request;
	};

Our response is always sequential, so we’ll redefine isSequential to return always true. Also we need to define some methods used by the calee : readData() for passing the data to the web view, abort() for aborting the request – we won’t need this so it’s dummy, bytesAvailable() to return how many bytes we’re returning:

	qint64 MyNetworkReply::readData(char *data, qint64 maxSize) {

	    if (offset < content.size()) {

	        qint64 number = qMin(maxSize, content.size() - offset);

	        memcpy(data, content.constData() + offset, number);

	        offset += number;

	        return number;
	    } else {

	        return -1;
	    }
	}

	qint64 MyNetworkReply::bytesAvailable() const {

	    return content.size() - offset + QIODevice::bytesAvailable();
	}

	bool MyNetworkReply::isSequential() const {

	    return true;
	}

Additionally, we added 3 new methods: one for appending a string to the content, one for appending a byte sequence and a method for finishing the reply:

	void MyNetworkReply::appendContent(QString str) {

	    content += str;
	}

	void MyNetworkReply::appendContent(QByteArray ccontent) {

	    content += ccontent;
	}

	void MyNetworkReply::finishReply() {

	    open(ReadOnly | Unbuffered);

	    setHeader(QNetworkRequest::ContentTypeHeader, request->ContentTypeHeader);
	    setHeader(QNetworkRequest::ContentLengthHeader, QVariant(content.size()));

	    QTimer::singleShot( 0, this, SIGNAL(readyRead()) );
	    QTimer::singleShot( 0, this, SIGNAL(finished()) );
	}

For finishing the reply we need to set some headers like content type, which we get from the initial request we already stored and the content length. Then we emit two signals, readyRead() and finished(). There is a small trick here. We cannot emit these signals imediatelly, since the reply was not returned yet by our QNetworkAccessManager, so there is no one actually listening for them. Instead we emit them later with a timer.

Next, let’s see how we use this new reply. For this we subclass QNetworkAccessManager. We gonna need to handle the zip file, so we store a pointer to this and the file location:

	class MyNetworkManager : public QNetworkAccessManager {

	    Q_OBJECT

	    public:
	        explicit MyNetworkManager(QNetworkAccessManager* oldManager, QObject *parent = 0);
	        QNetworkReply *createRequest(
	            QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
	            QIODevice *device);
	        ~MyNetworkManager();

	        void setZipFile(QString zipFile);

	    private:
	        QNetworkAccessManager* _oldManager;
	        QString _zipFile;
	        QuaZip* _smartClient;
	};

In the constructor itself we initialize the member variables and get some things from the old manager and in destructor we release the memory (Instead of this we could opt for having Qt/QObject handle it for us):

	MyNetworkManager::MyNetworkManager(QNetworkAccessManager* oldManager, QObject *parent) :
	    QNetworkAccessManager(parent),
	    _oldManager(oldManager),
	    _smartClient(NULL) {

	    setCache(_oldManager->cache());
	    setCookieJar(_oldManager->cookieJar());
	    setProxy(_oldManager->proxy());
	    setProxyFactory(_oldManager->proxyFactory());
	}

	MyNetworkManager::~MyNetworkManager() {

	    if (_smartClient) {

	        delete _smartClient;
	    }
	}

When the zip file’s location is set, we free the previous zip file related resources – if any and then open the new file:

	void MyNetworkManager::setZipFile(QString zipFile) {

	    _zipFile = zipFile;

	    if (_smartClient) {

	        delete _smartClient;
	    }

	    _smartClient = new QuaZip(zipFile);
	    _smartClient->open(QuaZip::mdUnzip);
	}

And lastly, the core of the class itself, the reason of subclassing: Processing the requests:

	QNetworkReply *MyNetworkManager::createRequest(
	    QNetworkAccessManager::Operation operation, const QNetworkRequest &request,
	    QIODevice *device) {

	    // myzip://js/isomorphic/system/modules/ISC_Core.js"
	    // url.filename = ISC_Core.js
	    // url.scheme = myzip
	    // url.path = /isomorphic/system/modules/ISC_Core.js

	    if (request.url().scheme() == "myzip") {

	        QString file = request.url().path();

	        if (file.startsWith("/")) {
	            file = file.right(file.length()-1);
	        }

	        if (!_smartClient->setCurrentFile(file)){

	            qDebug() << "Cannot open " << request.url().path();

	            return NULL;
	        } else {

	            QuaZipFile zf(_smartClient);
	            zf.open(QuaZipFile::ReadOnly);

	            MyNetworkReply* res = new MyNetworkReply((QNetworkRequest*)&request);

	            res->appendContent(zf.readAll());

	            zf.close();

	            res->finishReply();

	            return res;
	        }
	    } 

	    // none of our 'protocols', let the base class handle the request.
	    return QNetworkAccessManager::createRequest(operation, request, device);
	}

Last, we need to add our QNetworkAccessManager to our web view and start the ball rolling. We do this just after setting the web page and before retrieving the frame:

	ui->webView->setPage(new MyWebPage());

	QNetworkAccessManager *oldManager = ui->webView->page()->networkAccessManager();
	MyNetworkManager* manager = new MyNetworkManager(oldManager, ui->webView->page());

	manager->setZipFile("C:/testQtSmartClient/smartclient.zip");	// change this to your location of smartclient.zip

	ui->webView->page()->setNetworkAccessManager(manager);

	// .. code to follow ..

Oh and almost forgot: our ‘application’ stored in the resource file , under / prefix, as js/Application.js:

	isc.say("Hello World");

Build, run and you should get something like this:


Happy coding.

Leave a Comment