Для создания пользовательского интерфейса приложений придумано множество инструментов. Фреймворк Kirigami – как раз один из таких. Хотя он не отличается какой-то особой оригинальностью, Kirigami призван сделать создание приложений более простым и быстрым. С помощью этого инструмента можно создавать адаптивные и кроссплатформенные программы, то есть такие, которые работают не только на десктопах, но и на мобильных устройствах, причем не только под управлением GNU/Linux.
Появлением на свет этот фреймворк обязан сообществу KDE, которое развивает окружение рабочего стола Plasma и довольно богатый набор приложений для него. Существует также проект Plasma Mobile, позволяющий пользоваться окружением на мобильных устройствах. Сообщество активно использует Kirigami в своих разработках, создавая на этом фреймворке новые приложения и переписывая с его помощью старые.
При помощи Kirigami созданы такие приложения, как центр программ Discover, аудиопроигрыватель Elisa, графический калькулятор KAlgebra, системный монитор Plasma System Monitor, программа для синхронизации с мобильными устройствами KDE Connect, календарь и программа для управления задачами Kalendar и еще множество других. Весь список приложений от сообщества можно посмотреть на этой странице.
Создан этот фреймворк как надстройка над языком QML. Kirigami входит в состав KDE Frameworks и содержит свой набор контролов, но не может заменить все компоненты. Например, для кнопок и текстовых меток используется модуль QtQuick.Controls. Но уже в качестве текстовых полей можно использовать Kirigami.ActionTextField, хотя никто не запрещает пользоваться и обычным TextField из QtQuick.
В этой статье я бы хотел рассмотреть некоторые моменты из разработки простых приложений на этом фреймворке. Для разработки использовалась среда KDevelop — официальная среда разработки от KDE. Для всех приложений, рассматриваемых в этой статье, использовался один и тот же шаблон под названием Kirigami Application.
BMI Calculator
Это простой калькулятор индекса массы тела. Расчет производится на основе данных о росте, весе, половой принадлежности и размере окружности запястья. На выходе приложение не только показывает числовое значение ИМТ, но дополнительно отображает тип телосложения, верхнюю и нижнюю границы нормального веса, а также проверяет, находится ли вес в пределах нормы. Приложение имеет следующий внешний вид:
Репозиторий программы находится здесь. Следует сказать, что у меня есть точно такое же приложение, но написанное на GTK3/Vala для elementary OS. С ним можно ознакомиться здесь. Интерфейс GTK-версии калькулятора написан на XML и занимает целых 318 строк. Понятно, что UI, написанный на Vala, будет занимать гораздо меньше места. Что касается Kirigami-версии, то описание интерфейса в файле main.qml занимает всего 106 строчек. Довольно лаконично!
В качестве текстовых полей для ввода данных в программе используются уже упоминавшиеся ActionTextField:
Kirigami.ActionTextField {
id: weight
placeholderText: i18n("Weight in kg")
inputMethodHints: Qt.ImhFormattedNumbersOnly
Kirigami.FormData.label: i18n("Weight:")
rightActions: Kirigami.Action {
icon.name: "edit-clear"
visible: weight.text !== ""
onTriggered: {
weight.clear();
}
}
}
Все компоненты UI упаковываются в Kirigami.FormLayout, который, в свою очередь, входит в состав ColumnLayout. Последний упакован в Kirigami.ScrollablePage.
Для показа сообщения пользователю в программе применяется вот такое диалоговое окно:
Kirigami.PromptDialog {
id: alert
title: i18n("Attention!")
subtitle: i18n("Fill in all 3 fields!")
standardButtons: Kirigami.Dialog.Ok
}
В качестве меню используется Kirigami.GlobalDrawer с двумя пунктами:
globalDrawer: Kirigami.GlobalDrawer {
title: i18n("bmicalc")
titleIcon: "applications-graphics"
isMenu: !root.isMobile
actions: [
Kirigami.Action {
text: i18n("About BMI Calculator")
icon.name: "help-about"
onTriggered: pageStack.layers.push('qrc:About.qml')
},
Kirigami.Action {
text: i18n("Quit")
icon.name: "application-exit"
onTriggered: Qt.quit()
}
]
}
Для сохранения размеров окна и его расположения в этом и во всех остальных приложениях применяется вот такой код:
minimumWidth: Kirigami.Units.gridUnit * 20
minimumHeight: Kirigami.Units.gridUnit * 20
onClosing: App.saveWindowGeometry(root)
onWidthChanged: saveWindowGeometryTimer.restart()
onHeightChanged: saveWindowGeometryTimer.restart()
onXChanged: saveWindowGeometryTimer.restart()
onYChanged: saveWindowGeometryTimer.restart()
Component.onCompleted: App.restoreWindowGeometry(root)
Timer {
id: saveWindowGeometryTimer
interval: 1000
onTriggered: App.saveWindowGeometry(root)
}
Этот код содержится прямо в шаблоне проекта. То есть разработчику даже писать ничего не нужно. В шаблонах проектов GNOME Builder, когда писал на GTK, я такого не видел. Там если нужно написать нечто похожее, то придется делать это самостоятельно, например, через GSettings.
Вообще, особых проблем с калькулятором у меня не возникло, а вот со следующим приложением пришлось немного повозиться.
Desktop Files Creator
Это приложение предназначено для создания значков запуска. Такая программа может быть полезна в случае, когда у пользователя есть только исполняемый файл какого-либо приложения. Репозиторий данной программы можно найти здесь. Внешний вид программы:
С этим приложением возникли некоторые трудности. У меня до этого момента опыт программирования на QML равнялся нулю (в основном я занимался разработкой на GTK/Vala), и когда я дошел до необходимости сохранить файл в формате .desktop в папку .local/share/applications, то не нашел, как это сделать с помощью функций JavaScript.
Решить эту проблему можно путем создания нового файла fileio.cpp, в нем будут находиться нужные нам функции. Кроме функции сохранения файла понадобится еще парочка:
#include "fileio.h"
#include <QFile>
#include <QUrl>
#include <QTextStream>
FileIO::FileIO()
{
}
void FileIO::save(QString text, QString path){
QFile file(path);
if(file.open(QIODevice::ReadWrite)){
QTextStream stream(&file);
stream << text << endl;
}
return;
}
bool FileIO::exists(QString path){
QFile file(path);
return file.exists();
}
QString FileIO::toPath(QString path){
return QUrl(path).toLocalFile();
}
FileIO::~FileIO()
{
}
К этому файлу нужно еще создать заголовочный файл:
#include <QObject>
#ifndef FILEIO_H
#define FILEIO_H
class FileIO : public QObject
{
Q_OBJECT
public:
FileIO();
Q_INVOKABLE void save(QString text, QString path);
Q_INVOKABLE bool exists(QString path);
Q_INVOKABLE QString toPath(QString path);
~FileIO();
};
#endif // FILEIO_H
И последнее, что нужно будет сделать, — установить необходимое свойство контекста в main.cpp:
engine.rootContext()->setContextProperty("FileIO", new FileIO());
В этом же файле надо не забыть подключить заголовочный файл fileio.h. Теперь можно вызывать эти три функции из js-кода. С назначением функций save и exists все понятно, а вот зачем нужна функция toPath? Дело в том, что в программе используется компонент FileDialog и при вызове функции fileUrl, которая должна возвращать путь до выбранного файла, к пути добавляется ненужный префикс “file:///”. Вот функция toPath этот префикс и удаляет.
Еще хочется отметить то, что приложению необходимо имя домашней директории пользователя, чтобы построить путь до папки, где хранятся файлы значков запуска. Существует несколько способов узнать имя домашней директории пользователя, например можно сделать это через соответствующую переменную окружения. Для этого, как в случае с fileio.cpp, нужно создать соответствующий файл с именем qmlEnvironmentVariable.cpp и вот таким содержимым:
#include "qmlEnvironmentVariable.h"
#include <stdlib.h>
QString QmlEnvironmentVariable::value(const QString& name)
{
return qgetenv(qPrintable(name));
}
void QmlEnvironmentVariable::setValue(const QString& name, const QString &value)
{
qputenv(qPrintable(name), value.toLocal8Bit());
}
void QmlEnvironmentVariable::unset(const QString& name)
{
qunsetenv(qPrintable(name));
}
QObject *qmlenvironmentvariable_singletontype_provider(QQmlEngine *, QJSEngine *)
{
return new QmlEnvironmentVariable();
}
Не забываем про заголовочный файл:
#ifndef QMLENVIRONMENTVARIABLE_H
#define QMLENVIRONMENTVARIABLE_H
#include <QObject>
class QQmlEngine;
class QJSEngine;
class QmlEnvironmentVariable : public QObject
{
Q_OBJECT
public:
Q_INVOKABLE static QString value(const QString &name);
Q_INVOKABLE static void setValue(const QString &name, const QString &value);
Q_INVOKABLE static void unset(const QString &name);
};
// Define the singleton type provider function (callback).
QObject *qmlenvironmentvariable_singletontype_provider(QQmlEngine *, QJSEngine *);
#endif // QMLENVIRONMENTVARIABLE_H
Теперь надо не забыть подключить заголовочный файл и зарегистрировать все это дело в main.cpp:
qmlRegisterSingletonType<QmlEnvironmentVariable>("QmlEV", 1, 0, "EnvironmentVariable", qmlenvironmentvariable_singletontype_provider);
После импорта модуля QmlEV в файле main.qml можно использовать этот модуль для определения домашней директории пользователя и построения пути до нужной папки, например таким образом:
var path = EnvironmentVariable.value("HOME") + "/.local/share/applications/";
if(!FileIO.exists(path)){
showAlert("Attention!", "Path "+path+" does not exist! The program will not be able to perform its functions.")
return;
}
Все добавленные cpp-файлы нужно не забыть указать в сценарии CMakeLists.txt, расположенном в папке src.
Честно признаюсь, что эта необходимость иногда подключать код на C++ к файлу на QML, в котором пишется код на JavaScript, мне не очень понравилась. У меня есть такая же программа на GTK4/Vala, и при ее создании не нужно было писать какую-либо функцию на другом языке программирования. Все нужные методы находятся в стандартных библиотеках, и ими спокойно можно пользоваться из vala-кода.
Некоторую урезанность JavaScript в QML можно объяснить тем, что он рассчитан только на то, чтобы писать логику пользовательского интерфейса, а все остальное писать на C++. Сами создатели языка рекомендуют поступать именно так. Как писать свое приложение, решать, конечно же, самому разработчику. Если код будет содержать не 3 функции в отдельном cpp-файле, как в приведенных выше примерах, а, допустим, 30, то тут, как мне кажется, стоит задуматься над тем, чтобы писать всю логику только на плюсах, а интерфейс — на QML/JS.
Radio
Простенькое онлайн-радио с шестью встроенными станциями. Исходный код приложения расположен здесь. Приложение выглядит следующим образом:
В приложении используется компонент MediaPlayer из QtMultimedia. Этот модуль нужно импортировать в файле main.qml и прописать его в сборочном сценарии CMakeLists.txt, который находится в папке src. Там надо найти функцию target_link_libraries и добавить в имеющийся список Qt5::Multimedia. Далее переходим в корневой сценарий и в функции find_package для Qt5 дополняем список модулей:
find_package(Qt5 ${QT5_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg Multimedia)
Что мне особенно понравилось при разработке этого приложения, так это то, как буквально в одну строчку можно получить заголовок трансляции. Когда я писал свою версию радио на Vala, то пришлось создавать целый метод для получения этих заголовков. Подробнее об этом можно прочитать в этой статье. Здесь же задача решается гораздо проще:
MediaPlayer {
id: player
metaData.onMetaDataChanged: metaDataTitle.text = player.metaData.title
}
Каждой кнопке соответствует своя функция, которая запускает воспроизведение определенной станции. Вот пример одной из этих функций:
function playDeepHouse(){
heading.text = deepHouse.text
metaDataTitle.text = ""
player.stop()
player.source = "http://strm112.1.fm/deephouse_mobile_mp3"
player.play()
}
Для остановки воспроизведения используется кнопка в верхней части страницы:
actions.main: Kirigami.Action {
text: i18n("Stop")
icon.name: "media-playback-stop"
onTriggered: {
player.stop()
heading.text = "Stopped"
metaDataTitle.text = ""
}
}
Для отображения названия станций используется Kirigami.Heading, а для отображения заголовков трансляций – простая текстовая метка:
Kirigami.Heading {
id: heading
Layout.alignment: Qt.AlignCenter
}
Controls.Label {
id: metaDataTitle
Layout.alignment: Qt.AlignCenter
}
Чтобы добавить иконку для приложения, которая будет показываться в меню, нужно поместить изображение в формате svg в корень репозитория. Далее в desktop-файле заменить дефолтное значение для Icon на имя иконки без расширения и прописать команду установки в корневом сценарии CMakeLists.txt:
install(FILES org.kde.radio.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps/)
Репозиторий программы содержит манифест для сборки flatpak. Этот манифест сразу был в шаблоне проекта. В манифесте прописано все необходимое, но для радио потребуется добавить одну строку в раздел finish-args для доступа к PulseAudio и удалить одну ненужную, которая разрешает доступ к домашней папке пользователя. Должно получиться вот так:
"finish-args": [
"--share=ipc",
"--share=network",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri"
]
Для сборки flatpak лучше всего воспользоваться программой flatpak-builder. С помощью этой программы можно не только собрать, но и сразу произвести установку приложения:
flatpak-builder --force-clean --install --user /home/user-name/projects-directory/kderadio /home/user-name/projects-directory/kirigami-radio/org.kde.radio.json
Подробнее с flatpak-builder можно ознакомиться на этом сайте.
Проект также можно настроить для сборки пакета под Android. Для этого в настройках KDevelop во вкладке Android нужно указать пути до Android SDK и Android NDK. После этого в меню «Выполнение» надо переключить среду выполнения с основной системы на Android. Для получения более подробной информации лучше всего посетить страницу официальной документации.
В целом фреймворк мне понравился. Он позволяет достаточно легко и быстро создавать самые разные приложения для KDE Plasma, Android и других платформ. Проект существует с 2016-го года и за эти несколько лет не отправился на свалку истории, а наоборот — довольно успешно развивается. Надеюсь, что сообщество не забросит проект и продолжит развивать его дальше.
Автор статьи @KAlexAl
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
- 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS