23 February 2015

Гибридные приложения в Qt на примере использования D3.js

JavaScriptProgrammingC++QtData visualization
Tutorial
D3 — мощная JavaScript библиотека для визуализации данных. На мой взгляд — просто рай для web-разработчика, казалось бы недоступный для Qt-программиста. Но гибкость фреймворка Qt позволяет интегрировать web-frontend в толстый клиент с помощью механизма Qt Web Bridge. Такие приложения получили наименование гибридные (Qt Hybrid Apps).

Для JavaScript-программистов хорошая новость заключается в том, что их решения можно легко интегрировать в Desktop приложения, что потенциально может увеличить целевую аудиторию пользователей разрабатываемых библиотек (во всяком случае это верно для мира Qt приложений).

На скриншоте ниже изображен виджет Dependency Wheel (Круг Зависимостей), отрисовка которого осуществляется с помощью D3.js а управление данными и отображением — с помощью Qt. При нахождении указателя над соответствующей дугой её взаимосвязи «подсвечиваются», а остальные становятся полупрозначными. Данный виджет можно использовать для визуализации различного рода зависимостей (например библиотек).

В отличии от оригинального JS решения диаграмма динамически изменяет размер под размер виджета, а данные устанавливаются на стороне Qt, а не с помощью загрузки JSON-файла.

Статья больше ориентирована на Qt-программистов, но также может быть интересна и JS программистам.



Идея гибридных приложений


Отправной точкой идеи гибридных приложений является ряд ограничений, присущих нативным приложениям:
  • дополнительные расходы по внедрению и сопровождению клиентских частей системы;
  • написание уникального интерфейса пользователя порой является нетривиальной задачей;
  • невозможность повторного использовать API существующих веб-приложений.

Гибридные приложения решают эти проблемы за счет того, что:
  • развертывание выполняется как в веб приложениях;
  • сложные интерфейсы создаются с использованием web-технологий (HTML, CSS, SVG, Canvas);
  • повторно используется API существующих веб-приложений.

Архитектура гибридных приложений предполагает, что
  • Qt-приложение выступает в роли браузера;
  • взаимодействие с пользователем и логика приложения программируется в JavaScript;
  • дополнительная функциональность реализуется на С++ в Qt-части приложения.

Таким образом гибридные приложения реализуют идею тонкого клиента.
Одним из примеров гибридных приложений в Qt является WebKit Image Analyzer.

В примере, рассматриваемом в статье, будет использована только часть подхода гибридных приложений: отображение компонента за счет JavaScript. При этом все необходимые JS файлы будут расположены в ресурсах, как в классическом StandAlone приложении (автономном и не требующем для работы подключения к интранет/интернет сети).

Структура проекта


Общая структура файлов проекта изображена на рисунке:



В директории base находятся:
  • d3viewer.h и d3viewer.cpp — определение и реализация базового класса-вьювера D3Viewer, наследованного от QWidget и инкапсулирующего взаимодействие с QWebView.
  • d3webpage.h и d3webpage.cpp — определение и реализация D3WebPage — наследника QWebPage (для поддержки вывода сообщений об ошибках и отладочной информации в QWebPage::javaScriptConsoleMessage).

В директории charts/pie:
  • dependencywheelwidget.h и dependencywheelwidget.cpp — определение и реализация базового класса-вьювера, наследованного от QWidget и инкапсулирующего взаимодействие с QWebView.

Директория resources поделена на две: js и html. В html находится та страница, которая будет загружаться в виджете и в которой находится весь код взаимодействия с Qt, в js — необходимые для работы DependencyWheel js-файлы: общий для D3 — d3.min.js и специфичный для примера — d3.dependencyWheel.js.

Диаграмма классов


Для того, чтобы сократить объем статьи средствами VisualParadigm была создана простая диаграмма: в ней опущены атрибуты и методы классов, которые не имеют прямого отношения к описываемой технологии. Детали реализации можно узнать в исходниках, ссылка на которые находится в конце статьи.



Взаимодействие Qt <-> JS


В гибридных приложениях в JavaScript внедряется специальный объект, вызов методов которого обрабатывается на стороне Qt:

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

Этот метод вызывается в производных от D3Viewer классах в конструкторе перед загрузкой страницы:

addContextObject("api", this);

Далее взаимодействие Qt c JS возможно посредством четырех механизмов:
  1. Посредством обращения к свойствам объекта.
    для этого необходимо определить свойство в объекте, который является контекстным объектом в JS («api»):

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

    После этого можно обращаться к данным свойствам из JS:

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

  2. Обработкой сигналов Qt в JS, для этого в JS необходимо подключить соответствующую функцию-обработчик к сигналу.

    api.update.connect(redraw);
    

  3. Вызовом слотов Qt в JS, например при обработке клика по элементу:

          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. Вызовом других методов Qt в JS, для этого объявление метода нужно предварить макросом Q_INVOKABLE.

    Q_INVOKABLE void thisMethodIsInvokableInJavaScript();
    

  5. Непосредственным исполнением JS-кода.

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

В примере способы 4 и 5 не используются

Отладка JavaScript в гибридном приложении


Для отладки JS приложения (как и обзора DOM, просмотра сетевой активности, загружаемых ресурсов и т.д.) в конструкторе D3Viewer необходимо установить следующее свойство:

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

Тогда во время выполнения в контекстном меню (щелчок правой кнопки мыши по QWebView) будет доступно пункт контекстного меню «Проверить» (Inspect).



Выбрав который отображается окно Web-inspector-a.



В данном окне на вкладке Scripts можно включить отладку.



Установка breakpoint-а осуществляется кликом по соответствующему номеру строки слева.
P.S. В Qt 4.8.6 мне так и не получилось перехватить breakpoint. В 5.3.0 все работает штатно.

Недостатки


У любого решения есть как свои достоинства, так и недостатки. И в данном случае за «красивости» D3.js придется платить свою цену.
  • Дополнительные накладные расходы (в первую очередь памяти).
    Помимо того, что QWebView «тянет» за собой webkit, создавая новый «гибридный» виджет мы заново создаем достаточно тяжеловесный объект QWebView. Это не так актуально, если весь UI грузится в одном QWebView (как предлагается в оригинальной идее гибридных приложений).
  • Риск невозможности обратного повторного использования в web после модификации JS. Под нужды Qt можно так модифицировать JavaScript код, что он станет непригоден в web-приложении. Поэтому все обращения к Qt-объекту api желательно изолировать в одном месте — например в секции script html файла, который в данном случае будет разный для web и Qt приложения а JS код в подключаемых файлах будет единым.
  • Баги WebKit-а в Qt 4.8.6
    В D3 активно используются древовидные структуры, описание которых находится в JSON файлах. На стороне Qt формируется такой же JSON объект посредством комбинации QVariantMap/QVariantList приведенных в итоге к QVariant. Несмотря на то, что структура, таких объектов идентична, в Qt 4.8.6 все же есть отличия, так как напрямую такой объект не воспринимается и приходится повторно «пересоздать» JSON объект в памяти на стороне JS. В Qt 5.3.0 такой костыль можно не использовать — все работает напрямую.

    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;
    }
    


    Еще в Qt 4.8.6 после 15-20 секундного ресайза виджета приложение перестает штатно работать и вываливается ворох сообщений о ошибке в JS. В Qt 5.3.0 все работает штатно, что опять же наводит на мысль о том, что проблема кроется в реализации самого WebKit-а (хотя я могу заблуждаться). Однако вопросы выделения и освобождения памяти на стороне JS остаются актуальными.

Исходный код


Исходный код примера доступен по ссылке.
Пример собирался и запускался под Qt 4.8.6 и 5.3.0.
Tags:qtD3javascriptc++
Hubs: JavaScript Programming C++ Qt Data visualization
+21
25.9k 167
Comments 13
Popular right now
С++ разработчик (Qt)
from 80,000 ₽Бизнес.РуУфа
C++ разработчик
from 80,000 ₽TRUSTSOFTКраснодар
Программист С++
from 150,000 ₽SDTМосква
C++ Developer
from 130,000 to 180,000 ₽QuadcodeСанкт-Петербург
Senior system developer/ С++
to 170,000 ₽GETMOBITМосква