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


С помощью библиотеки 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-приложения на его основе.


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

Комментарии (5)


  1. Synopticum
    14.02.2025 10:35

    Это же обычный вебвью, не считая кода бриджа (чтобы самому не писать), есть ли какой то смысл использовать ваши готовые компоненты?


    1. NickWare Автор
      14.02.2025 10:35

      Webview действительно "обычный", а вот система бриджа интегрирована с UI библиотекой (которая на js написана). Без неё не будет визуальных компонентов, их придётся реализовывать самостоятельно, что весьма трудозатратно. Использование библиотеки существенно упрощает и ускоряет разработку графических интерфейсов.


  1. eugenk
    14.02.2025 10:35

    Спасибо ! Не знал об этой библиотеке. Забрал её в закладки. А вообще я наверное уже лет 5 использую такой подход. Правда не через QT, а в браузере. Если моему приложению (C/C++, Java и т.п.) требуется GUI, делаю простейший http-сервер и запускаю GUI в браузере. Последняя моя попытка в 2018-м написать нативный GUI для приложения кончилась печально. Тем, что через два года из стандартной сборки JRE выкинули JavaFX, на котором он был написан. И приложение перестало работать "ис кароппки". С тех пор только браузер.

    P.S. Кстати, если библиотека как я понимаю Ваша. Вы не хотели бы продублировать документацию на русском языке ??? Было бы ващще круто. Полезная либа, да с русскоязычной документацией. А я в качестве ответного шага могу написать интерфейсный файл для связи с scala.js (мой основной инструмент для веба).


    1. NickWare Автор
      14.02.2025 10:35

      Соглашусь, что локализованная документация на русском языке имеет смысл. Спасибо за обратную связь.

      Насколько я понимаю, интеграция со scala.js предполагает реализацию бриджа?

      P.S. Да, помню ту ситуацию с JavaFX, долго тогда хотелось желать разработчикам "всего хорошего".


  1. xtraroman
    14.02.2025 10:35

    Вопрос немного не в тему статьи: Почему выбрали Qt в Нейтрино, а не авалонию, например.