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


С помощью библиотеки facefull можно создавать современные графические пользовательские интерфейсы с использованием технологий HTML, CSS и JS как для веб, так и для нативных приложений. Библиотека содержит более 30 различных визуальных компонентов с огромными возможностями кастомизации. Все компоненты адаптивные и отлично подходят для использования с разными разрешениями экрана, а также с тачскринами.


Библиотека обладает исчерпывающей документацией, а ее исходный код доступен в публичном репозитории.


В случае с нативными приложениями, в качестве рендера интерфейса выступает системный веб-движок, в случае Нейтрино — это WebKit. В Нейтрино имеется поддержка Qt5, поэтому самый простой способ отображения такого интерфейса — использование компонента QWebView. Недавно мы рассказывали о нашем инструменте мониторинга аномальной активности, пользовательский интерфейс графического приложения разработан с использованием facefull.


Введение


Отличие использования facefull для разработки интерфейсов для нативных приложений от классических случаев применения web-based UI в том, что в данном случае в webview переносится только графический интерфейс (и логика его работы), а вся основная логика приложения остаётся написанной на нативных языках С\С++. Это означает, что приложение не теряет в функциональных возможностях и скорости работы.
Для того, чтобы связать нативный код на С\С++ и код facefull требуется реализация специального механизма обмена сообщениями, который называется bridge. Для этих целей была разработана библиотека facefull-bridge, которая реализует этот механизм для различных фреймворков, в том числе и Qt5WebKit. Она включает в себя все компоненты библиотеки facefull. Библиотека facefull-bridge была портирована под ОС Нейтрино и доступна "из коробки". Её исходный код также доступен в публичном репозитории.


Схематически процесс организации взаимодействия компонентов внутри приложения выглядит следующим образом:


Таким образом, применение facefull для построения графических интерфейсов в нативных приложениях сводится к трём простым шагам.


Шаг 1. Создание главного окна Qt с виджетом QWebView


Обычно главное окно в Qt создаётся с помощью наследования от класса QMainWindow. На главном окне необходимо разместить только виджет QWebView и ряд вспомогательный компонентов. Типовой вариант заголовочного файла mainwindow.h выглядит так:


#ifndef MAINWINDOW_H  
#define MAINWINDOW_H  

#include <QMainWindow>  
#include <QVBoxLayout>

QT_BEGIN_NAMESPACE  
namespace Ui { class MainWindow; }  
QT_END_NAMESPACE  

class MainWindow : public QMainWindow {  
    Q_OBJECT  
private:  
    Ui::MainWindow *ui;  
    QVBoxLayout *MainLayout;  
    QWidget *MainWidget;  
    QWebView *WebView;

public:  
    MainWindow(QWidget *parent = nullptr);  
    ~MainWindow();  
};  

#endif //MAINWINDOW_H

Реализация класса в файле mainwindow.cpp содержит создание объекта класса QWebView и размещение виджета на главном окне:


#include <iostream>  
#include <QDir>  
#include "mainwindow.h"  
#include "ui_mainwindow.h"  

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui -> setupUi(this);
    // Если нужно убрать системную рамку и заголовок окна
    setWindowFlags(Qt::FramelessWindowHint);  

    std::cout << "WebKit version: " << qWebKitVersion().toStdString() << std::endl;  

    MainLayout = new QVBoxLayout();  
    MainLayout -> addSpacing(0);  
    MainLayout -> setContentsMargins(0, 0, 0, 0);  
    WebView = new QWebView(this);  
    MainLayout -> addWidget(WebView);  
    MainWidget = new QWidget();  
    MainWidget -> setLayout(MainLayout);  
    setCentralWidget(MainWidget);  
}  

MainWindow::~MainWindow() {  
    delete ui;  
}

Шаг 2. Инициализация bridge


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


В описание класса в файле mainwindow.h нужно внести следующие изменения:


#include <facefull/bridge/qt5webkit.hpp>

class MainWindow : public QMainWindow {  
    Q_OBJECT
private:  
    ...
    FacefullBridgeQt5WebKit *Bridge;
    ...
protected:  
    bool eventFilter(QObject* object, QEvent* event) override;  

public slots:  
    void doBridgeEventReceive(const QString&) const;  
signals:  
    void BridgeEventHandler(QString, QString);
...
};

Перегруженный метод eventFilter необходим для реализации перемещения окна с помощью кастомного заголовка, а методы doBridgeEventReceive и BridgeEventHandler — слот и сигнал для обработки событий от QWebView.


В файле mainwindow.cpp добавится создание объекта класса FacefullBridgeQt5WebKit и реализация некоторых из указанных методов:


MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ...
    Bridge = new FacefullBridgeQt5WebKit(this, WebView, QUrl("путь к html странице"));
    ...

}

void MainWindow::doBridgeEventReceive(const QString &data) const {  
    Bridge -> doEventCatch(data.toStdString());  
}  

bool MainWindow::eventFilter(QObject* object, QEvent* event) {  
    Bridge -> doMoveWindow((QMouseEvent*)event);  
    return false;  
}

Здесь переменная respath содержит путь к странице, реализующей графический интерфейс.


Шаг 3. Реализация UI


Этот шаг сводится к созданию трёх компонентов: HTML страницы, стилей (CSS), и основного JS-скрипта интерфейса приложения. Традиционно файлы называются window.html, style.css и app.js соответственно. Библиотека facefull в свою очередь предоставляет реализацию функционала визуальных компонентов, реализацию внутренней составляющей bridge и стили.


Описание HTML страницы

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


<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Facefull test</title>
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
        <!-- Подключение необходимых ресурсов -->
        <script src="facefull/facefull.min.js" charset="utf-8"></script>
        <script src="src/app.js" charset="utf-8"></script>
        <link rel="stylesheet" href="facefull/facefull.min.css">
        <link rel="stylesheet" href="src/style.css">
    </head>
    <body>
      <!-- Контейнер окна, обязательно должен иметь id="W" -->
        <div id="W" class="Window">
            <!-- Определение заголовка окна, обязательно должен иметь id="WH" -->
            <div id="WH" class="WindowHeader" onselectstart="return false">
                <div class="WindowIcon"></div>
                <div class="WindowCaption">Facefull app example</div>
                <div class="WindowMover"></div>
                <div class="WindowControlsBlock">
                    <div class="WindowControl Min"><div></div></div>
                    <div class="WindowControl Max" id="WCM"><div></div></div>
                    <div class="WindowControl Close"><div></div></div>
                </div>
            </div>
            <!-- Контейнер рабочей зоны окна, обязательно должен иметь id="G" -->
            <div id="G" class="GlobalArea">
                <!-- Определение главного бокового меню -->
                <div class="MainMenu">
                    <div id="MMI" class="MainMenuItems">
                        <div class="TooltipTarget" data-pagename="Page1" data-tooltip-text="Page 1" data-tooltip-width="130" data-tooltip-pos="right"></div>
                        <div class="TooltipTarget" data-pagename="Page2" data-tooltip-text="Page 2" data-tooltip-width="120" data-tooltip-pos="right"></div>
                    </div>
                </div>
                <!-- Рабочая область окна -->
                <div class="WorkArea">
                    <!-- Определение первой вкладки. В id указывается желаемое id вкладки с префиксом P (т.е. P<id вкладки>). Оно автоматически связывается с элементами главного меню -->
                    <div id="PPage1" class="Page">
                        <div class="Title">
                            <div class="TitleText"><div>Вкладка 1</div>
                                <div class="Subtitle">Подзаголовок вкладки 1</div>
                            </div>
                        </div>
                        <div class="Box PageBody Scrolling" data-scrollboxname="P1SB">
                            <div class="Scrolldata">
                                <!---->
                            </div>
                        </div>
                    </div>
                    <!-- Определение второй вкладки -->
                    <div id="PPage2" class="Page">
                        <div class="Title">
                            <div class="TitleText"><div>Вкладка 2</div>
                                <div class="Subtitle">Подзаголовок вкладки 2</div>
                            </div>
                        </div>
                        <div class="Box PageBody Scrolling" data-scrollboxname="P1SB">
                            <div class="Scrolldata">
                                <!---->
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!-- Определения дополнительных элементов окна: -->
            <!-- Определение всплывающей подсказки -->
            <div id="TT" class="Tooltip"></div>
            <!-- Определение затеняющего оверлея для всплывающих сообщений -->
            <div id="OV" class="Overlay"></div>
            <!-- Определение стандартного окна всплывающих сообщений -->
            <div id="AE" class="Alert Hidden Rounded">
                <div class="AlertCaption"></div>
                <div class="AlertText"></div>
                <div class="AlertButtons">
                    <div id="AB-OK" class="Button Rounded">OK</div>
                    <div id="AB-Y" class="Button Rounded">Yes</div>
                    <div id="AB-N" class="Button Rounded">No</div>
                </div>
            </div>
        </div>
    </body>
</html>

Описание стилей

Стандартный файл стилей facefull.min.css содержит все необходимые стили для стандартных визуальных компонентов библиотеки facefull, но их можно переопределить в собственном CSS файле. Например, можно задать значки пунктам главного меню и значок в заголовке окна:


.MainMenu *[data-pagename="Page1"]::before {  
    content: '\F056E';  
}  

.MainMenu *[data-pagename="Page2"]::before {  
    content: '\F0D7C';  
}  

.WindowIcon {  
    font-family: "Material Design Icons";  
    font-size: 28px;  
}  

.WindowIcon::before {  
    content: '\F126F';  
}

В состав библиотеки facefull входит шрифт Material Design Icons, содержащий сотни значков в минималистичном стиле.


Описание основного JS скрипта

Теперь необходимо описать скрипт инициализации графического интерфейса. JS файл (app.js) должен содержать следующие обязательные определения:


// Инициализация объекта facefull. Все взаимодействия с библиотекой осуществляются через этот объект
facefullCreate(true);

// Запуск инициализации интерфейса после загрузки страницы
window.addEventListener('load', function () {
    App();
});

function App() {
    // Инициализация компонентов facefull
    facefull.doInit();

    // ...

    // Иницализация графического интерфейса всегда должна заканчиваться отправкой в bridge сообщения doWindowReady. Это сообщение генерирует событие, которое означает, что интерфейс проинициализирован и готов к работе. После этого можно отправлять и получать сообщения через bridge.
    facefull.doEventSend("doWindowReady");
}

Использование QRC для сборки ресурсов

Все ресурсы графического интерфейса можно использовать как напрямую с файловой системы, так и через QRC. QRC позволяет "вкомпилировать" их в бинарный файл, что удобнее при распространении приложения. Пример файла описания ресурсов выглядит следующим образом:


 <!DOCTYPE RCC><RCC version="1.0">  
    <qresource prefix="/">  
        <file>ui/window.html</file>  
        <file>ui/app.js</file>  
        <file>ui/style.css</file>  
        <file>ui/facefull/facefull.min.js</file>  
        <file>ui/facefull/facefull.min.css</file>  
        <file>ui/facefull/theme-light.min.css</file>  
        <file>ui/facefull/fonts/md-embedded.woff</file>  
    </qresource>
</RCC>

Таким образом, все ресурсы будут доступны из основного кода приложения через префикс qrc:. Теперь нужно указать в конструкторе при создании bridge правильный путь к html странице графического интерфейса:


    Bridge = new FacefullBridgeQt5WebKit(this, WebView, QUrl("qrc:/ui/window.html"));

Теперь после запуска приложения можно увидеть получившийся результат:



Взаимодействие с UI через bridge

Со стороны нативного кода отправка сообщений через bridge осуществляется с помощью метода doEventSend, для приёма сообщений необходимо создать обработчик события с помощью метода doEventAttach. Аналогичным образом взаимодействие осуществляется и со стороны UI — отправка и приём выполняется с помощью методов facefull.doEventSendи facefull.doEventHandlerAttach соответственно.


Рассмотрим пример. Чтобы отправить тестовое сообщение в UI в нативном коде (например в конструкторе класса MainWindow) выполняем соответствующий вызов:


// Навешиваем обработчик на событие готовности окна и отправляем сообщение
Bridge -> doEventAttach("doWindowReady", [this](const std::string& data) {  
    Bridge -> doEventSend("doTestMessage", "Тестовое сообщение");  
});

В свою очередь в app.js добавляем обработчик события:


facefull.doEventHandlerAttach("doTestMessage", function(data) {  
    AlertShow("Сообщение", data, "info", "OK"); 
});

Теперь при запуске приложения будет появляться всплывающее сообщение с текстом "Тестовое сообщение":



Заключение


Библиотека facefull-bridge (библиотека визуальных компонентов facefull входит в её состав) является open-source проектом, который был портирован и адаптирован под использование в ОС Нейтрино. Описанный в статье подход позволяет с минимальными усилиями начать создавать современные графические интерфейсы, например, для вывода таблиц, графиков, элементов управления или других важных показателей ФПО. При этом сохраняется производительность и функциональность этого ФПО, так как нативный код приложения остаётся нативным.


Полный исходный код описанного в статье примера использования библиотеки facefull-bridge доступен в публичном репозитории СВД ВС. Компоненты библиотеки станут доступны потребителям ОС Нейтрино с релизом редакции 2024, однако сейчас библиотеку можно собрать руками из оригинального репозитория.


Список поддерживаемых визуальных компонентов, доступных "из коробки", постоянно расширяется. Сейчас доступны различные кнопки, переключатели, списки, графики, поля ввода, ��еню и много другое. Кроме визуальных компонентов facefull предоставляет и другие возможности. Менеджер тем оформления, с помощью которого можно достаточно просто управлять стилями оформления и создавать новые (стандартные визуальные компоненты "из коробки" доступны в двух стилях — тёмном и светлом); менеджер локализаций, позволяющий управлять локализациями интерфейса; а также менеджер отображения, предоставляющий возможность описывать правила поведения графического интерфейса при изменении разрешения и устройства отображения.


Использование библиотеки не требует специальных знаний (только чтение документации на API), так как применяются стандартные HTML5, CSS и JS. Полученный графический интерфейс приложения легко переносится из нативного режима в браузер, если потребуется создание web-приложения на его основе.


Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей