Гібридні програми в Qt на прикладі використання D3.js

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 не використовуються

Недоліки

У будь-якого рішення є як свої переваги, так і недоліки. І в даному випадку за «красивості» D3.js доведеться платити свою ціну.
  • Додаткові накладні витрати (в першу чергу пам'яті).
    Крім того, що QWebView «тягне» за собою webkit, створюючи новий «гібридний» віджет ми заново створюємо досить важкий об'єкт QWebView. Це не так актуально, якщо весь UI вантажиться в одному QWebView (як пропонується в оригінальній ідеї гібридних додатків).
  • Складність налагодження. Якщо у нативній додатку можна цілком точно визначити, що йде «не так», запустивши програму в режимі налагодження, то гібридне додаток не дозволяє нам поставити точку зупину у JS скрипті і подивитися, що коїться на кухні того ж D3.js. Частково проблему вирішує налагоджувальну інформацію за допомогою console.log(«something»), але виведення повідомлень про помилки з JS часто буває мало інформативним.
  • Ризик неможливості зворотного повторного використання в 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.