Разработка для Sailfish OS: отображение графиков с использованием D3.js и QML Canvas

  • Tutorial
Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке приложений для мобильной платформы Sailfish OS. На этот раз речь пойдет о работе с графиками в Sailfish-приложении. Мы расскажем о поиске и подключении библиотеки и о том, как мы отображаем графики математических функций. Отметим, что предложенное решение не ограничивается платформой Saiflsh OS и в целом подходит для любого QtQuick приложения.

Описание задачи


Мы решили создать приложение-калькулятор, который бы удовлетворял нуждам инженеров, студентов и школьников, работающих с устройствами под управлением Sailfish OS. Наше приложение должно было содержать следующие компоненты:

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

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

  1. Подключить внешнюю библиотеку наподобие QuickQanava.
  2. Использовать объект QML Canvas.
  3. Реализовать свой собственный компонент на C++ и подключить его к приложению.

Библиотека QuickQanava работает с Qt 5.8, который пока не доступен на платформе Sailfish OS. Объект QML Canvas позволяет использовать высокоуровневый язык JavaScript, а также предоставляет API, совместимый со стандартом W3C, что открывает возможности по использованию сторонних библиотек.

Ввиду того, что отображение графика не требует серьёзных вычислений и не надо часто перерисовывать сцену, мы решили использовать в проекте именно QML Canvas с привлечением внешней JavaScript-библиотеки.

QML Canvas и Context2D


Элемент QML Canvas позволяет рисовать прямые и изогнутые линии, простые и сложные фигуры, графики и ссылки на графические изображения. Он также может рисовать текст, цвета, тени, градиенты и шаблоны, а также выполнять манипуляцию изображением на уровне пикселя. Выход Canvas помимо отображения на экране может быть сохранен как файл изображения или сериализован в URL.

Рендеринг на холст выполняется с использованием объекта Context2D, как правило, в результате обработки сигнала paint. Сам объект реализует спецификацию HTML Canvas 2D Context, которая также реализована в объекте HTML Canvas, что позволяет применять JavaScript-библиотеки, разработанные для использования в веб-браузерах, для QML-приложений. В настоящее время трёхмерный контекст объектом Context2D не поддерживается.

Рассмотрим простейший пример подключения QML Canvas к вашему приложению:

import QtQuick 2.0
Canvas {
    id: mycanvas
    width: 100
    height: 200
    onPaint: {
        var ctx = getContext("2d");
        ctx.fillStyle = Qt.rgba(1, 0, 0, 1);
        ctx.fillRect(0, 0, width, height);
    }
}

Первой строкой в примере мы подключаем QtQuick 2.0, затем определяем элемент Canvas и задаем параметры id, width и height. Ввиду того, что элемент сам по себе не имеет элементов и может занимать произвольное пространство, то ему необходимо указать размеры либо явно задав ширину и высоту, либо связав края элемента с другими элементами на странице. Если вы не укажите размер, то элемент не будет виден. В примере мы используем первый подход.

Сигнал paint вызывается при активации элемента Canvas. Его обработка происходит в методе onPaint. В нем мы получаем контекст для отображения и сохраняем его в переменной ctx. Полное описание параметров для getContext можно, например, здесь. Будьте внимательны, Qt предоставляет доступ только к двухмерному контексту отображения.

Далее мы используем контекст для отображения прямоугольника. ctx.fillStyle задает цвет заливки прямоугольника. Первые три параметра определяют цвет по компонентам красной, зелёной и синей, а четвёртый компонент определяет прозрачность. ctx.fillRect(x, y, w, h) рисует его используя x и y как координаты начала, а w и h в качестве ширины и высоты.

Весь список методов контекста, которые можно использовать для рисования, можно посмотреть в официальной документации. Мы не будем рассматривать все методы в данной статье, отметим только, что координаты у изображения начинаются в левом-верхнем углу. Ось OX растёт вправо, а ось OY сверху-вниз.

Использование внешних библиотек


Конечно поставленную перед нами задачу, мы могли бы решить напрямую с использованием API Context2D, однако мы решили рассмотреть возможность использования внешних библиотек. Благодаря тому, что данный API доступен во всех основных браузерах, разработчики под Sailfish OS могут воспользоваться большим количеством существующих библиотек, которые облегчают реализацию целевых функций. В нашем приложении мы решили использовать библиотеку D3.js.

Краткий обзор D3.js


D3.js представляет собой библиотеку на языке JavaScript для обработки и визуализации данных. В настоящее время D3.js является одним из наиболее популярных фреймворков, используемых для графической обработки данных и создания всякого рода диаграмм и графиков.

Сам D3.js является большим проектом, который позволяет решать множество задач, поэтому не существует единого способа интеграции данной библиотеки в HTML-приложения. Мы использовали достаточно простой подход для интеграции, но другие тоже должны сработать успешно в ваших приложениях.

Интеграция D3.js в QML-приложение


Сначала библиотеку необходимо скачать и обеспечить её доступность на целевом устройстве. Напоминаем, что QML-компоненты на Sailfish OS не компилируются в ресурсы, а поставляются в виде отдельных файлов. В результате все зависимости от JavaScript тоже желательно поставлять в виде отдельных файлов.

D3.js поставляется в отдельном файле, который называется d3.js, а также минифицированной версией, которая находится в файле d3.min.js. В ходе разработки мы выяснили, что минифицированная версия не загружается корректно QML-движком, поэтому рекомендуется использовать полный вариант — он работает без нареканий.

Для нашего приложения мы разместили файл d3.js в каталоге qml/pages нашего проекта. Всё содержимое данного каталога копируется на целевое устройство, поэтому файл также копируется вместе с проектом. Также файл был включён в список DISTFILES в QML-проекте, чтобы QtCreator показывал его в списке других файлов.

Создание компонента для отображения графика


В рамках приложения нам необходимо отображать графики трёх функций на двухмерной плоскости. Все рассматриваемые функции зависят от значения абсциссы. Для качественного отображения их на отрезке мы решили вычислять промежуточные значения на отображаемом в настоящий момент отрезке.

Общую логику по построению мы вынесли в отдельный компонент Plot. Он обеспечивает следующую функциональность:

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

В конкретных местах использования нам остаётся определить только 1 функцию, которая будет вычислять значения графика.

Рассмотрим структуру базового компонента.

import QtQuick 2.0
import "d3.js" as D3
Canvas {
  // Определение различных свойств
  onPaint { } // Отображение графика
  // Набор утилитарных функций
  Item {
    PinchArea {} // Обработка жеста щипка
    MouseArea {} // Обработка перетаскивания
  }
}

Сначала мы подключаем необходимые нам библиотеки: набор компонентов QtQuick, а также саму библиотеку D3.js. Подключение JavaScript-файлов похоже на подключение других QML-файлов. Для решения этой задачи также используется ключевое слово import.

Полную информацию про подключение JavaScript-файлов можно прочитать в официальной документации. Основной аспект при проведении импорта — это указание имени, через которое будут доступны все функции, определённые в данном документе. В нашем коде мы дали название этому объекту D3.

Корневым элементом Plot является Canvas, на котором мы и отображаем информацию. Для проведения вычислений и обработки жестов в данном элементе мы определили набор свойств и функций. Ключевой из них является onPaint — обработчик события отрисовки изображения.

Дочерним элементом по отношению к Canvas является Item, который представляет собой всего лишь контейнер для объектов PinchArea и MouseArea. Данные объекты были добавлены для обработки жеста-щипка, для управления уровнем приближения, и перетаскивания, для управления положением координатных осей. Обработчики данных жестов обновляют координаты, которые используются при отрисовке графика.

Краткий обзор процесса отображения


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

Для отображения ключевых элементов: координатной сетки и графика функции — используется функция d3.line. Данная функция позволяет отображать произвольные ломаные и прямые линии. Входом для функции служит массив из данных. Для того, чтобы ей воспользоваться, необходимо настроить следующие параметры:

  • Настроить генераторы для получения ординаты и абсциссы из элемента массива.
  • Указать способ соединения линий между собой.
  • Указать графический контекст, на котором необходимо отобразить информацию.

Рассмотрим пример формирования изображения линии графика.

var context = plot.getContext('2d');
var xScale = d3.scaleLinear()
  .range([leftMargin, width])
  .domain([minX, maxX]);
var yScale = d3.scaleLinear()
  .range([height - bottomMargin, 0])
  .domain([minY, maxY]);
var line = d3.line().x(function (d) {
    return xScale(d[0]);
  }).y(function (d) {
    return yScale(d[1]);
  }).curve(d3.curveNatural).context(context);

Сначала мы настраиваем шкалы, d3.scaleLinear, которые упрощают нам работу с масштабированием графика. Достаточно указать физические границы изображения в вызове метода range() и границы графика в вызове метода domain(). Формируются шкалы для абсциссы, ординаты и записываются в переменные xScale и yScale, соответственно.

Затем мы описываем линию, которая в качестве параметров будет принимать массив значений графика. В вызов метода x() передаём функцию, которая извлекает первый элемент массива и преобразует его с помощью шкалы xScale. Похожая функция передаётся и в качестве аргумента вызова метода y(), только обращение происходит ко второму элементу массива. Потом мы настраиваем способ связи между элементами, в нашем случае это d3.curveNatural. D3.js поддерживает огромное количество вариантов построения кривых, о них можно прочитать в официальной документации. В конце построения линии мы связываем её с графическим контекстом нашего изображения.

Для отрисовки линии достаточно вызывать созданную линию и передать ей массив из необходимых координат:

line([[1, 2], [2, 15], [3, 8], [4, 6]])

Похожим образом строятся линии для отображения осей.

Стоит отметить, что в начале каждой отрисовки холст полностью очищается. Это необходимо для того, чтобы предыдущее изображение не мешало отображению текущего состояния. А новые изображения появляются, когда пользователь изменяет масштаб графика или изменятся границы отображения элементов.

Использование компонента Plot


С помощью данного компонента мы реализовали отображение трёх графиков для каждой из функций. Для каждого графика мы создали по странице. На этих страницах происходит вычисление значений для отображения графика.

Общая структура каждого графика показана ниже.

Page {
  property var elem
  property var border
  property var rootLine
  id: page
  backNavigation: plot.controlNavigation()
  Plot {
    id: plot
    anchors.margins: Theme.horizontalPageMargin
    width: parent.width
    height: parent.height
    function drawPlot(line) {
      line(getPoints());
    }
    function getPoints() {
      // Вычисление значений для отображений.
    }
  }
}

Корневым элементом выступает страница, которая помещается на стек для отображения графика. В качестве параметров передаются коэффициенты уравнения, начальные границы для отображения графика, а также линия, показывающая местоположение корней функции.

Далее мы отключаем навигацию назад в том случае, если пользователь взаимодействует с графиком. Это позволяет предотвратить случайные возвращения из страницы с помощью жестов.
Единственным элементом на странице является элемент Plot. Мы явно указываем, чтобы он занял всё доступное пространство, оно будет использовано для отображения графика. Мы также определяем метод drawPlot. Данный метод будет вызываться каждый раз, когда потребуется заново отобразить функцию.

В качестве аргумента ему передаётся линия, которая была настроена, как было показано выше, в элементе Plot. Мы вызываем её и передаём ей результат работы метода getPoints(). Последний метод формирует набор точек, который будет специфичен для каждого отдельного графика.

Приложение Matrix Calculator


Мы надеемся, что с использованием этой информации вы с лёгкостью сможете реализовать подобную функциональность в своём приложении. Вы можете также более детально познакомиться с реализацией функций в приложении Matrix Calculator, установив его из репозитория OpenRepos.net, или посмотреть на работу с библиотекой в исходном коде, который доступен в репозитории на BitBucket.

Скриншоты приложения приведены ниже:




UPD: Добавили скриншоты приложения.
Поделиться публикацией

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

    +4
    Ну а скриншотиков как оно по итогам получается?
      +1
      Всё интереснее и интереснее! :) Да, и присоединяюсь: скриншоты было бы хорошо добавить.
        0
        JS библиотеки, для рисования графиков, тащились когда не было альтернатив. Но потом открыли Qt Charts и Qt Data Visualization, поэтому очень странное решение.
          +2
          Зависит от ситуации. Если приложение должно быть закрытым, а денег на коммерческую лицензию Qt нет, то Qt Charts и Qt Data Visualisation становятся недостижимой роскошью.
            0
            Слушайте, а как это их помесячное лицензирование работает? Я могу купить лицензию на 1 месяц, получить Qt Charts, завернуть его в свой модуль и сказать «разработка данного модуля закончена», а дальше пилить остальной код под LGPL, используя в нём ранее созданный модуль?
              0
              Я не знаю.
                0

                Нет, с начала этого года все лицензии Qt стали "подписочные" и наличие активной подписки требуется и на период разработки, и на период распространения. До начала этого года ещё существовали "пожизненные" лицензии, и там можно было один раз купить, и право и на разработку, и на распространение сохранялось за вами навсегда.


                Другой момент, что лицензия Qt запрещает "смешивать" коммерческий и Open Source код Qt в одном проекте. Должно быть или-или, так что в вашем сценарии даже "пожизненная" лицензия не помогла бы. А если вы будете Qt Charts использовать под Open Source, то это GPLv3, и весь ваш проект автоматически станет GPLv3 со всеми вытекающими.

                  0
                  То есть фактически Qt послала лесом всех мелких разрабов? Или платите 500 баксов в месяц ПОЖИЗНЕННО или пользоваться нельзя, так?
                    0

                    Ну тут особо ничего нового, компания и раньше не отличалась большим интересом к малому бизнесу.


                    Хотя вот есть ещё вариант для стартапов.

            0
            Спасибо за статью.

            Напоминаем, что QML-компоненты на Sailfish OS не компилируются в ресурсы, а поставляются в виде отдельных файлов. В результате все зависимости от JavaScript тоже желательно поставлять в виде отдельных файлов.

            QML-файлы и другие ресурсы возможно вложить в QRC, только для этого необходимо править не только .pro, но и .yaml.
            Тоже самое про Qt Charts и Qt Data Visualization: их можно использовать в Sailfish, но сборка чуть сложнее.
              0
              Qt Charts собраны.
              build.omprussia.ru/package/binaries/home:aakulich:qt:5.9/qtcharts?repository=latest_i486
              build.omprussia.ru/package/binaries/home:aakulich:qt:5.9/qtcharts?repository=latest_armv7hl
              Для использования достаточно установить библиотеки на устройство или в эмулятор.
                0
                В ходе разработки мы выяснили, что минифицированная версия не загружается корректно QML-движком
                Баг не заводили по теме?
                Кстати, ввиду того, что в Qt используется свой JS-движок QV4, очень стало интересно, насколько он проигрывает* V8 в таких задачах?
                * Я не проверял, но почти уверен, что тюненный вдоль и поперек V8 все же быстрее специализированного QV4 (если мне не изменяет память, его фишка в более прозрачном переходе из JS контекста в C++, нашлось это).

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

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