Hybrid applications in Qt using D3.js as an example

  • Tutorial
D3 is a powerful JavaScript library for data visualization. In my opinion, it’s just a paradise for a web developer, seemingly inaccessible to a Qt programmer. But the flexibility of the Qt framework allows you to integrate the web-frontend into a thick client using the Qt Web Bridge mechanism . Such applications are called hybrid ( Qt Hybrid Apps ).

For JavaScript programmers, the good news is that their solutions can be easily integrated into Desktop applications, which can potentially increase the target audience of users of developed libraries (in any case, this is true for the world of Qt applications).

The screenshot below shows the Dependency Wheel widget(Circle of Dependencies), rendering of which is carried out using D3.js and data and display control - using Qt. When the pointer is located above the corresponding arc, its relationships are “highlighted”, and the rest become semi-digit. This widget can be used to visualize various kinds of dependencies (e.g. libraries).

Unlike the original JS solution, the chart dynamically resizes to fit the widget, and the data is set on the Qt side, rather than by loading a JSON file.

The article is more focused on Qt-programmers, but may also be interesting for JS programmers.



The idea of ​​hybrid applications


The starting point for the idea of ​​hybrid applications is a number of limitations inherent in native applications:
  • additional costs for the implementation and maintenance of client parts of the system;
  • writing a unique user interface is sometimes a non-trivial task;
  • the inability to reuse the API of existing web applications.

Hybrid applications solve these problems by:
  • Deployment is performed as in web applications;
  • complex interfaces are created using web-technologies (HTML, CSS, SVG, Canvas);
  • reused API of existing web applications.

The hybrid application architecture assumes that
  • Qt-application acts as a browser;
  • user interaction and application logic are programmed in JavaScript;
  • additional functionality is implemented in C ++ in the Qt part of the application.

Thus, hybrid applications implement the idea of ​​a thin client.
One example of hybrid applications in Qt is WebKit Image Analyzer .

In the example considered in the article, only part of the hybrid application approach will be used: displaying the component through JavaScript. In this case, all the necessary JS files will be located in the resources, as in the classic StandAlone application (stand-alone and not requiring an intranet / Internet network connection to work).

Project structure


The general structure of the project files is shown in the figure:



The base directory contains:
  • d3viewer.h and d3viewer.cpp - definition and implementation of the base view class D3Viewer , inherited from QWidget and encapsulating interaction with QWebView .
  • d3webpage.h and d3webpage.cpp - definition and implementation of D3WebPage - the descendant of QWebPage (to support the output of error messages and debugging information in QWebPage :: javaScriptConsoleMessage ).

In the charts / pie directory:
  • dependencywheelwidget.h and dependencywheelwidget.cpp - definition and implementation of the base view class inherited from QWidget and encapsulating interaction with QWebView .

The resources directory is divided into two: js and html. The html contains the page that will be loaded in the widget and which contains all the Qt interaction code, the js contains the js files necessary for DependencyWheel to work: d3.min.js common for D3 and d3.dependencyWheel, which is specific to the example. js.

Class diagram


In order to reduce the volume of the article using VisualParadigm tools, a simple diagram was created: it omitted the attributes and methods of classes that are not directly related to the described technology. Details of the implementation can be found in the sources, a link to which is at the end of the article.



Qt <-> JS Interaction


In hybrid applications, a special object is implemented in JavaScript, the method call of which is processed on the Qt side:

void D3Viewer::addContextObject(const QString &name, QObject *object)
{
    frame()->addToJavaScriptWindowObject( name, object ); //frame() - QWebFrame
}

This method is called in D3Viewer-derived classes in the constructor before the page loads:

addContextObject("api", this);

Further interaction of Qt with JS is possible through four mechanisms:
  1. By accessing the properties of the object.
    To do this, you must define the property in the object, which is the context object in JS ("api"):

    public:
    Q_PROPERTY(float padding READ padding WRITE setPadding)
    public slots:
        float padding(); //getter
        void setPadding(const float padding); //setter
    

    After that, you can access these properties from JS:

        var chart = d3.chart.dependencyWheel()
                               .width(api.width)
                               .height(api.height)
                               .margin(api.margin)
                               .padding(api.padding);
    

  2. By processing Qt signals in JS, for this, in JS, you must connect the corresponding function-handler to the signal.

    api.update.connect(redraw);
    

  3. By calling Qt slots in JS, for example, when processing a click on an element:

          g.append("svg:path")
            .style("fill", fill)
            .style("stroke", fill)
            .attr("d", arc)
            .on("mouseover", fade(0.1))
            .on("mouseout", fade(1))
            .on('click', function (d) { api.itemClicked(packageNames[d.index]) } ); //здесь подключается обработчик
    

  4. By calling other Qt methods in JS, for this, the method declaration must be preceded by the Q_INVOKABLE macro.

    Q_INVOKABLE void thisMethodIsInvokableInJavaScript();
    

  5. Direct execution of JS code.

    void D3Viewer::evaluateScript(const QString &script)
    {
        frame()->evaluateJavaScript(script);
    }
    

In the example, methods 4 and 5 are not used.

JavaScript debugging in a hybrid application


To debug a JS application (as well as browse the DOM, view network activity, loaded resources, etc.), the following property must be set in the D3Viewer constructor:

#ifdef QT_DEBUG //в этом случае контекстное меню будет доступно только в отладочной сборке
    page()->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
#endif

Then, at runtime, the context menu item “Inspect” will be available in the context menu (right-click on QWebView).



By choosing which window the Web-inspector-a is displayed.



In this window, on the Scripts tab, you can enable debugging.



Installation of breakpoint is done by clicking on the corresponding line number on the left.
PS In Qt 4.8.6, I did not manage to intercept the breakpoint. In 5.3.0, everything works properly.

disadvantages


Any solution has its advantages and disadvantages. And in this case, for the "prettiness" D3.js will have to pay its price.
  • Additional overhead (primarily memory).
    In addition to the fact that QWebView “pulls” a webkit after itself, creating a new “hybrid” widget, we re-create a rather heavyweight QWebView object. This is not so true if the entire UI is loaded in one QWebView (as suggested in the original idea of ​​hybrid applications).
  • The risk of the inability to reverse reuse on the web after modifying JS. To the needs of Qt, you can modify JavaScript code so that it becomes unusable in a web application. Therefore, it is desirable to isolate all calls to the api Qt object in one place - for example, in the script section of the html file, which in this case will be different for the web and Qt applications and the JS code in the connected files will be the same.
  • WebKit bugs in Qt 4.8.6
    D3 actively uses tree structures described in JSON files. On the Qt side, the same JSON object is formed through a combination of QVariantMap / QVariantList resulting in QVariant. Despite the fact that the structure of such objects is identical, in Qt 4.8.6 there are still differences, since such an object is not directly perceived and you have to repeatedly “recreate” the JSON object in memory on the JS side. In Qt 5.3.0, such a crutch can not be used - everything works directly.

    function recreateJsonObject(obj)
    {
        var jsonObj = {};
        for(key in obj) {
          jsonObj[key] = obj[key];
          var dependencies = [];
          for (var i = 0 ; i < obj[key].length ; i++ )
          {
            dependencies.push(obj[key][i]);
          }
          jsonObj[key] = dependencies;
        }
        return jsonObj;
    }
    


    Even in Qt 4.8.6, after a 15-20 second resize of the widget, the application stops working normally and a heap of error messages in JS falls out. In Qt 5.3.0, everything works properly, which again suggests that the problem lies in the implementation of WebKit itself (although I can be mistaken). However, the issues of allocating and freeing memory on the JS side remain relevant.

Source


The source code for the example is available here .
An example was built and run under Qt 4.8.6 and 5.3.0.

Also popular now: