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

  • 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.

Комментарии 13

    +4
    Какие страшные буквы на первой картинке….
      +1
      Да, верно, но у меня с шрифтами в системе все очень плохо пока. Пожалуйста, посмотрите на пример на сайте — там должно быть не так страшно (у меня и там страшно).
      +4
      Некоторые замечания:
      1. frame()->addToJavaScriptWindowObject() нужно вызывать не вручную перед загрузкой страницы, а в обработчике сигнала QWebFrame::javaScriptWindowObjectCleared, который вызывается непосредственно перед загрузкой новой страницы. Это позволит перезагружать страницу без опасения, что объекты не внедрятся.
      2. Отлаживать страницы можно и нормальным отладчиком. Для этого для страницы нужно установить атрибут DeveloperExtrasEnabled, например так:
      page()->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);

      После чего отладчик вызывается из контекстного меню страницы.

      Подробный список атрибутов и настроек QWebSettings
        +1
        Спасибо Вам большое, я доработал статью, добавив раздел «Отладка JavaScript в гибридном приложении» и удалив соответствующий пункт о недостатках. Может Вы заодно подскажете, как активировать опцию «Always enabled», чтобы отладка запускалась в момент создания QWebView? (у меня она не имеет эффекта и «сбрасывается» при каждом запуске приложения). Буду очень Вам признателен.
          0
          У меня тоже не получилось её включить. В качестве решения могу предложить после загрузки страницы в отладчике зайти на страницу Audits, выбрать пункт Reload Page and Audit on Load. В таком случае все брекпоинты и активация отладки сохраняются (до выхода из приложения).
            0
            Спасибо, для этого видимо нужно будет выполнить еще и пункт 1 ваших замечаний. В представленном коде «Reload» приводит к пустой странице.
        +1
        А точно логику приложения приходится реализовывать на JavaScript? — А то для все преимущества данного подхода из-за этого сходят на нет.
          +1
          Нет не обязательно — так просто озвучено в идее гибридных приложений. В примере JS использовался только для отображения. Как и в случае использования С++ с QML (на мой взгляд) в JS целесообразно реализовывать только функционал, отвечающий за интерфейс, а бизнес-логику (domain model) стоит оставить в C++.
            0
            Вот и я удивился. QML используем только как интерфейс, в пример только как интерфейс, а вот для JS иначе и это странно. Если уж бизнес-логика на JS, то зачем что-то писать на плюсах?

            Рад, что это не так
          0
          Спасибо, интересно. А не знаете, возможно интегировать webapp-ориентированный JS код в QML приложение не сильно заморачиваясь? Пробовал в лоб, ругалось на window is undefined, а переписывать с привязкой к QML лениво…
            0
            Я в лоб не пробовал. Использовал только тот JS, который взаимодействует с объектами QML. Как вариант — использовать QGraphicsProxyWidget. Примеры можно посмотреть в этой ветке.
            Но лично мне этот вариант не очень нравится.
            0
            Это круто, спасибо.

            Несмотря на то, что для меня смерть десктопных приложений уже кажется неизбежной, но вот такие слияния технологий — могут очень крутые вещи рождать.
              0
              «Риск невозможности обратного повторного использования в web после модификации JS.»
              В этом случае можно просто воспользоваться веб-сокетами. Будет один и тот же JS-код как для локального приложения, так и для веб-приложения.
              Начиная с Qt5.3 они являются встроенными.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое