Компактная кросс‑платформенная open source C++ библиотека для разработки приложений с графическим интерфейсом — Frenchie

от автора

В этой статье я расскажу про возможности компактной кросс‑платформенной библиотеки с открытым исходным кодом для разработки приложений с графическим интерфейсом на языке C++ — Frenchie. Цель материала — познакомить читателей с возможностями библиотеки и сделать сравнение функционала с другими похожими проектами. Если кому-то интересна техническая сторона проекта, то предлагаю заглянуть в исходный код, либо в материалы, описывающие архитектурную и техническую часть.

Зачем делать свою библиотеку и что не так с уже существующими популярными решениями ?

Достаточно популярными современными C++ framework‑ами для разработки приложений с графическим интерфейсом являются Qt и WxWidgets. Данные framework‑и относительно просты в освоении, очень хорошо и подробно задокументированы, а также предоставляют широкие возможности для разработки приложений с графическим интерфейсом.

Основная проблема этих двух framework‑ов (и похожих на них) — это необходимость синхронизации состояния графического интерфейса и данных, по которым он рисуется (более подробно можно прочитать здесь). Именно по этой причине я долго искал решения, которые позволили бы рисовать интерфейс по любым данным и моделям «на лету».

Парадигма immediate mode user interface.

Архитектурный подход к построению графического интерфейса, используемый в Qt и WxWidget, заключается в том, чтобы представить элементы графического интерфейса в виде дерева элементов, имеющих свое состояние, и раз в какое-то время обходить это дерево, рисуя его элементы и отслеживая связанные с ними события. При этом, само дерево создается заранее и, как правило, сильно не изменяется по ходу работы программы. Такой подход к построению графического интерфейса называется retained mode user interface (интерфейс с хранением состояния).

Основной проблемой подхода retained mode user interface является синхронизация состояния интерфейса с состоянием данных, используемых для его рисовки. Отсюда вытекает логичный вопрос: а можно ли как-то рисовать интерфейс «на лету» по абсолютно любой модели данных ? Да, так делать можно, а сам подход называют immediate mode user interface (интерфейс с рисовкой «на лету»). Суть immediate mode user interface заключается в том, чтобы процедурно рисовать и перестраивать интерфейс по любой модели данных, а также хранить как можно меньше состояния, связанного с самим интерфейсом.

Библиотеки, реализующие парадигму immediate mode user interface.

Библиотек, реализующих парадигму immediate mode user interface не очень много. Наверное, это связано с нишевостью их использования (в основном разработка игр). Но, пожалуй, самая популярная библиотека, реализующая парадигму immediate mode user interface, — это библиотека ImGUI. ImGUI — прекрасный инструмент, закрывающий примерно 99% всех возможных задач, т.к библиотека предоставляет API для процедурного рисования интерфейсов практически любой сложности. Так, почему бы не использовать эту библиотеку ?

Основная проблема, с которой я столкнулся при работе с ImGUI — это отсутствие возможности полноценно рисовать 2D графику даже несмотря на то, что в библиотеке есть встроенный 2D renderer. Почему так? Дело в том что, 2D renderer ImGUI не поддерживает геометрические преобразования (поворот, масштабирование, перенос и.т.д), а также ручную сортировку рисуемых объектов в глубину для корректного отображения полупрозрачных объектов при наложении их друг на друга.

Также, у бибилотеки ImGUI достаточно высокий порог входа, т.к для того, чтобы ее запустить, потребуется потратить какое-то количество времени на изучение основ компьютерной графики и разобраться в каком-то конкретном графическом API.

Еще одна проблема библиотеки ImGUI — это отсутствие средств автоматической компоновки элементов графического интерфейса. Что имеется в виду? В библиотеках типа того же Qt существуют стандартные средства компоновки элементов графического интерфейса, такие как горизонтальный/вертикальный стек, сетка и панель. В ImGUI такого, к сожалению нет, потому-что библиотека не хранит иерархию элементо. Вместо этого, на поверхности каждого окна располагается виртуальный курсор, который двигается по мере добавления виджетов и прочих элементов. Управлять размерами и расположением элементов на поверхности окна можно, но, это все придется делать самостоятельно.

Цель проекта Frenchie и какие возможности в него закладывались

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

В проекте Frenchie я хотел совместить простоту и гибкость парадигмы immediate mode user interface с удобным 2D rendering-ом, а также обеспечить минимальный порог входа в технологию, чтобы кому-то, кто захочет делать интерфейсы, не пришлось разбираться с нуля с используемыми под капотом графическими API и сторонними библиотеками.

Также, при создании библиотеки хотелось сделать не просто immediate mode, хотелось добавить в него алгоритмов автоматической компоновки элементов графического интерфейса, чтобы при использовании библиотеки приходилось писать как можно меньше кода, связанного с тем, как правильно располагать и компоновать элементы внутри окна. На этом можно было бы закончить, но, мне хотелось добиться кое чего еще.

immediate mode user interface — это, по сути, процедурное рисование элементов графического интерфейса. Процедурное рисование — это, конечно, хорошо, т.к мы можем рисовать и перестраивать интерфейс по абсолютно любым данным. В чем тогда проблема?

Проблема в том, что C++ — это компилируемый язык программирования и до тех пор, пока мы программу не запустим, мы не увидим наш интерфейс. Т.е, каждый раз, когда мы захотим внести какие-то изменения нам нужно будет закрыть программу, внести изменения, перекомпилировать программу и запустить ее заново. В принципе, когда проект небольшой, то никаких проблем с перекомпиляцией и запуском нет, но, вот если проект огромный, то время перекомпиляции даже куска программы может занимать время. Вопрос в том, а можно ли как-то сделать так, чтобы хотя бы интерфейс можно было менять и сразу видеть результат без перезапуска программы?

Допустим, у нас с вами уже есть модуль процедурного рисования интерфейса «на лету», тогда, мы можем выбрать любой удобный для нас формат файла, например JSON или XML, в этом файле по каким-то правилам верстать интерфейс, а программа будет подхватывать изменения файла, парсить его и рисовать интерфейс.

Далее, мы можем написать некую модель данных, которая будет хранить абсолютно любые данные, включая callback-и, по принципу «ключ-значение» и сделать к этой модели данных переопределяемый контроллер, который будет эту модель данных заполнять и обновлять.

В таком случае у нас получится не просто верстать интерфейс из файла, но и привязывать к элементам интерфейса какие-то данные, что даст нам возможность отделить разработку интерфейса программы от разработки ее логики, а также менять верстку не перекомпилируя проект или отдельные его части. Вот такие интересные возможности я хотел реализовать. Теперь посмотрим, что из этого всего вышло на деле. Начнем, пожалуй, с базовой части Frenchie: с того, как он работает.

Как работает Frenchie ?

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

Цикл работы приложения разбит на несколько этапов, на каждом из которых приложение вызывает у всех слоев абстракции набор переопределяемых функций. Для запуска контекстного окна и рисования графики Frenchie использует системный и графический backend-ы.

Системный backend — это статический класс, который оборачивает платформо-зависимый функционал операционной системы, предназначенный для открытия контекстного окна и отслеживания связанных с ним событий. На момент написания этой статьи, Frenchie использует сторонние библиотеки в качестве системного backend-а.  Ниже представлен список сторонних библиотек, работа с которыми поддержана во Frenchie:

Библиотека

SDL3

GLFW

Графический backend — это статический класc, который оборачивает платформо-зависимый функционал, отвечающий за загрузку геометрии и текстур на видеокарту для последующей рисовки, т.е по сути, графический backend является оберткой вокруг графического API.Ниже приведен список графических API, работа с которыми поддержана в актуальной на момент написания этой статьи версии Frenchie:

Графический API

OpenGL3

DirectX9

MacOS Metal

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

2D Renderer

2D renderer во Frenchie представляет из себя очередь API независимых rendering команд, которые выполняются rendering backend-ом в порядке сортировки рисуемых объектов в глубину. Сортировка rendering команд по глубине расположения рисуемых объектов обеспечивает корректный расчет цветов на их поверхностях при их наложении друг на друга.

При помощи 2D renderer-а Frenchie мы можем менять clear color (цвет заливки котекстного окна), scissor box (ограничивающих прямоугольник, за пределами которого графика обрезается), а также рисовать текст и полигональные геометрические фигуры без отверстий. На любую геометрическую фигуру можно натягивать текстуры, либо заливать любым цветом, включая градиентый цвет.

Текстуры также как и ttf шрифты загружаются в память видеокарты и удаляются из нее функциями rendering backend-а. Для полигнальных геометрических фигур со скругленными краями предусмотрена настройка точности аппроксимации скругления — tessellation tolerance, которую можно изменять. В принципе, описанный функционал позволяет рисовать практически любую 2D графику. В качестве примера работы с 2D renderer-ом Frenchie приведу следующий код:

#include <FrenchieApplication.hpp>#include <FrenchieApplicationLayerRenderingQueue2D.hpp>class ClockRenderingExample : public Frenchie::Application::Layer{public:    ClockRenderingExample(){}    virtual ~ClockRenderingExample(){}    virtual bool awake() override    {        // загружаем слой 2D renderer-а        if(m_Renderer == nullptr)            m_Renderer = Frenchie::Application::App::push_layer<Frenchie::Application::RenderingQueue2D>();        return m_Renderer != nullptr;    }    virtual void frame_update()    {        int        depth                = 0;        gs_2d_boxf clockBoundingBox     = m_Renderer->current_viewport();        float      clockFaceRadius      = gs_min(clockBoundingBox.width(), clockBoundingBox.height()) * 0.5f;        gs_color   clockFrameColor      = gs_color_rgb(128, 128, 128);        gs_color   clockBackgroundColor = gs_color_rgb(32, 32, 32);        m_Renderer->push_clear_color(gs_color_rgba(32, 128, 128, 128));        // рисуем рамку        m_Renderer->push_arc_filled(            clockBoundingBox.center(),            clockFaceRadius,            clockFaceRadius,            0.f,            360.f,            clockFrameColor,            m_Renderer->calculate_transform_matrix((float)++depth));        // рисуем задний фон        m_Renderer->push_arc_filled(            clockBoundingBox.center(),            clockFaceRadius - 8.f,            clockFaceRadius - 8.f,            0.f,            360.f,            clockBackgroundColor,            m_Renderer->calculate_transform_matrix((float)++depth));        // рисуем панель с часами        {            const float deltaAngle  = 360.f / 12.f;            const float sourceAngle = 0.f;            const float targetAngle = 360.f;                        for (float angle = gs_min(sourceAngle, targetAngle); angle < gs_max(sourceAngle, targetAngle); angle += deltaAngle)            {                // засечки                m_Renderer->push_line(                    clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(angle)), sin(gs_to_radians(angle))) * 0.9f,                     clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(angle)), sin(gs_to_radians(angle))),                    12.f,                    clockFrameColor,                    m_Renderer->calculate_transform_matrix((float)++depth));                // цифры                std::string text = Frenchie::Core::String::to_string<int>((int)(13.f - (targetAngle - angle) / targetAngle * 12.f));                float fontSize = 64.f * clockFaceRadius / 512.f;                m_Renderer->push_text(                    clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(angle + 300.f)), sin(gs_to_radians(angle + 300.f))) * 0.8f -                        m_Renderer->calculate_bounding_box(text.begin(), text.end(), fontSize, Frenchie::Application::ApplicationRenderingBackendFont()).size() * 0.5f,                    text.begin(),                    text.end(),                    fontSize,                    clockFrameColor,                    m_Renderer->calculate_transform_matrix(                        (float)++depth));            }        }        // панель с минутами        {            const float deltaAngle  = 360.f / 60.f;            const float sourceAngle = 0.f;            const float targetAngle = 360.f;            const float radius      = gs_min(clockBoundingBox.width(), clockBoundingBox.height()) * 0.5f;                        for (float angle = gs_min(sourceAngle, targetAngle); angle < gs_max(sourceAngle, targetAngle); angle += deltaAngle)            {                m_Renderer->push_line(                    clockBoundingBox.center() + radius * gs_vec2f(cos(gs_to_radians(angle)), sin(gs_to_radians(angle))) * 0.95f,                     clockBoundingBox.center() + radius * gs_vec2f(cos(gs_to_radians(angle)), sin(gs_to_radians(angle))),                    12.f,                    clockFrameColor,                    m_Renderer->calculate_transform_matrix((float)++depth));            }        }        // стрелки часов        {            m_Renderer->push_arc_filled(                clockBoundingBox.center(),                16.f,                16.f,                0.f,                360.f,                clockFrameColor,                m_Renderer->calculate_transform_matrix((float)++depth));            // часовая стрелка            float hourAngle = (float)(Frenchie::Core::Clock::local_time_hour() - 13) / 12.f * 360.f + 300.f;                        m_Renderer->push_arrow(                clockBoundingBox.center(),                clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(hourAngle)), sin(gs_to_radians(hourAngle))) * 0.5f,                12.f,                32.f,                clockFrameColor,                m_Renderer->calculate_transform_matrix((float)++depth));            // минутная стрелка            float minuteAngle = (float)(Frenchie::Core::Clock::local_time_minute() - 65) / 60.f * 360.f + 300.f;                        m_Renderer->push_arrow(                clockBoundingBox.center(),                clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(minuteAngle)), sin(gs_to_radians(minuteAngle))) * 0.6f,                12.f,                32.f,                clockFrameColor,                m_Renderer->calculate_transform_matrix((float)++depth));            // секундная стрелка            float secondsAngle = (float)(Frenchie::Core::Clock::local_time_second() - 65) / 60.f * 360.f + 300.f;                        m_Renderer->push_arrow(                clockBoundingBox.center(),                clockBoundingBox.center() + clockFaceRadius * gs_vec2f(cos(gs_to_radians(secondsAngle)), sin(gs_to_radians(secondsAngle))) * 0.7f,                4.f,                32.f,                clockFrameColor,                m_Renderer->calculate_transform_matrix((float)++depth));        }    }    virtual void finish() override    {        if(m_Renderer != nullptr)            m_Renderer->close();    }protected:    std::shared_ptr<Frenchie::Application::RenderingQueue2D> m_Renderer {nullptr};};int main(int argc, char *argv[]){    (void)argc;    (void)argv;    Frenchie::Application::App::push_layer<ClockRenderingExample>();    return Frenchie::Application::App::execute();}

Этот код нарисует нам вот такие вот аналоговые часы на мониторе:

Аналоговые часы, нарисованные 2D Renderer-ом Frenchie

Аналоговые часы, нарисованные 2D Renderer-ом Frenchie

Помимо рисования на мониторе, Frenchie может рисовать в текстуру, которую можно показывать в каком-нибудь виджете. Следующий пример показывает, как нарисовать какую-то графику в текстуру и потом отобразить ее в окне:

#include <FrenchieApplication.hpp>#include <FrenchieApplicationLayerRenderingQueue2D.hpp>class FrameBufferRenderingExample : public Frenchie::Application::Layer{public:    FrameBufferRenderingExample(){}    virtual ~FrameBufferRenderingExample(){}    virtual bool awake() override    {        // создаем два renderer-а и добавляем слой UI в приложение        if(m_Renderer1 == nullptr)            m_Renderer1 = Frenchie::Application::App::push_layer<Frenchie::Application::RenderingQueue2D>();        if(m_Renderer2 == nullptr)            m_Renderer2 = Frenchie::Application::App::push_layer<Frenchie::Application::RenderingQueue2D>();        if(m_UI == nullptr)            m_UI = Frenchie::Application::App::push_layer<Frenchie::Application::ImmediateUserInterfaceContextLayer>();        return m_UI != nullptr && m_Renderer1 != nullptr && m_Renderer2 != nullptr;    }    virtual void frame_update() override    {        // создаем окно и показываем в нем две нарисованные в текстуру сцены        if(m_UI->begin_window(            m_UI->next_id("Framebuffers test window", "Framebuffers test window"),            Frenchie::Application::ImmediateUserInterfaceNodeSettings_::ImmediateUserInterfaceNodeSettings_Defaults,            &m_Opened))        {            if(m_UI->begin_horizontal_stack(m_UI->next_id("Framebuffers")))            {                m_UI->image(m_UI->next_id("Scene-1"), gs_color_rgb(255, 255, 255), m_Renderer1->get_framebuffer_texture());                m_UI->image(m_UI->next_id("Scene-2"), gs_color_rgb(255, 255, 255), m_Renderer2->get_framebuffer_texture());                m_UI->end_horizontal_stack();            }            m_UI->end_window();        }        // рисуем сцену номер 1        {            m_Renderer1->render_to_texture();            int depth = 0;            m_Renderer1->push_rectangle_filled(                m_Renderer1->current_viewport().Min,                m_Renderer1->current_viewport().Max,                gs_color_rgb(32, 16, 16),                m_Renderer1->calculate_transform_matrix((float)depth++));            gs_2d_boxf box                   = m_Renderer1->current_viewport();            float      ballRadius            = gs_max(m_Ball.MajorRadius, m_Ball.MinorRadius);            gs_vec2f   ballToCenterDirection = gs_vector_normalize(box.center() - m_Ball.Center);            if(!box.contains(m_Ball.Center + ballRadius * ballToCenterDirection * -1.f) || !m_Direction.has_value())                m_Direction = gs_vector_normalize(ballToCenterDirection + gs_vec2f(gs_pseudo_random(-1.f, +1.f), gs_pseudo_random(-1.f, +1.f)));            m_Ball.Center += m_Direction.value() * 8.f;            m_Renderer1->push_arc_filled(                m_Ball.Center,                m_Ball.MinorRadius,                m_Ball.MajorRadius,                0.f,                360.f,                gs_color_rgb(128, 128, 128),                m_Renderer1->calculate_transform_matrix((float)depth++));        }        // рисуем сцену номер 2        {            m_Renderer2->render_to_texture();            int depth = 0;            m_Renderer2->push_arc_filled(                m_Renderer2->current_viewport().center(),                gs_min(m_Renderer2->current_viewport().width(), m_Renderer2->current_viewport().height()) * 0.5f,                gs_min(m_Renderer2->current_viewport().width(), m_Renderer2->current_viewport().height()) * 0.5f,                0.f,                360.f,                gs_color_rgb(32, 16, 16),                m_Renderer2->calculate_transform_matrix((float)depth++));        }    }    virtual void finish() override    {        if(m_Renderer1 != nullptr)            m_Renderer1->close();        if(m_Renderer2 != nullptr)            m_Renderer2->close();    }private:    std::shared_ptr<Frenchie::Application::ImmediateUserInterfaceContextLayer> m_UI        {nullptr};    std::shared_ptr<Frenchie::Application::RenderingQueue2D>                   m_Renderer1 {nullptr};    std::shared_ptr<Frenchie::Application::RenderingQueue2D>                   m_Renderer2 {nullptr};    std::optional<gs_vec2f>                                                    m_Direction {std::optional<gs_vec2f>()};    gs_2d_ellipsef                                                             m_Ball      {gs_2d_ellipsef(gs_vec2f(0.f, 0.f), 64.f)};};int main(int argc, char *argv[]){    (void)argc;    (void)argv;    Frenchie::Application::App::push_layer<FrameBufferRenderingExample>();    return Frenchie::Application::App::execute();}

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

Пример рисования в текстуру и ее отображения в окне

Пример рисования в текстуру и ее отображения в окне

Модуль процедурного рисования интерфейса.

Итак, в прошлом разделе мы в кратце ознакомились с тем, как работает 2D renderer Frenchie. Теперь посмотрим, что интересного есть в библиотеке Frenchie для рисования интерфейсов. Для рисования интерфейса, Frenchie имеет два программных модуля: модуль процедурного рисования интерфейса и модуль декларативного рисования интерфейса.

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

1. Окна с возможностью крепления вкладками или дочерними элементами:

Возможности по рисованию окон

Возможности по рисованию окон

2. Модальные диалоговые окна с возможностью наложения blur-а на блокируемые элементы интерфейса:

Возможности по рисованию диалоговых окон

Возможности по рисованию диалоговых окон

3. Виджеты: кнопки, label-ы, а также виджеты для скалярного и текстового ввода, progress bar-ы, color picker-ы и.т.д:

Пример того, что может Frenchie в части рисования различного рода виджетов

Пример того, что может Frenchie в части рисования различного рода виджетов

4. Деревья и таблицы. При этом, таблицы имеют возможность выделения видимой части по вертикали и горизонтали, что позволяет эффективно рисовать таблицы по несколько десятков тысяч строк и столбцов без потери производительности. Таблицы также имеют возможность полной кастомизации ячеек с данными, ячеек с названиями строк и столбцов, а также угловой ячейки, т.е мы можем верстать абсолютно любой content внутри ячеек таблицы:

Пример отображения огромной таблицы с данными с использованием Frenchie

Пример отображения огромной таблицы с данными с использованием Frenchie

5. 2D графики, векторные и круговые диаграммы:

Демонстрация возможностей Frenchie по рисованию 2D графиков

Демонстрация возможностей Frenchie по рисованию 2D графиков

6. Механика Drag and drop:

Демонстрация механики drag and drop

Демонстрация механики drag and drop

7. Выпадающие меню, менюбары, а также всплывающие подсказки:

Рисование выпадающих меню и всплывающих подсказок

Рисование выпадающих меню и всплывающих подсказок

8. Алгоритмы автоматической компановки элементов интерфейсов. На момент написания этой статьи реализован горизонтальный и вертикальный стек, а также сетка, панель и прокручиваемая область:

Демонстрация работы алгоритмов автоматической компоновки элементов графического интерфейса

Демонстрация работы алгоритмов автоматической компоновки элементов графического интерфейса

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

Модуль декларативного рисования интерфейса

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

Допустим, нам нужно нарисовать круговую диаграмму с несколькими секторами и возможностью включать/выключать легенду. С использованием модуля декларативного рисования интерфейса задача разбивается на два этапа: верстка и разработка логики. Начнем с верстки. Набросаем вот такой вот JSON файл с версткой:

{"VerticalStack":{"Name":"View","ScrollArea":{"Name":"LegendEditor","Settings":{"ResizeToContentsVertically":true},"SliderButton":{"Name":"ShowLegendButton","Checked":{"Source":"ShowLegend"}},"Label":{"Name":"ShowLegendLabel","Text":"Show legend","SameLine":true}},"HorizontalStack":{"Name":"Stack","Plot":{"Name":"PieChart","PlotPie":{"Name":"Pie","Names":{"Source":"PieNames"},"Values":{"Source":"PieValues"},"Colors":{"Source":"PieColors"},"Count":{"Source":"PieCount"}}},"PlotLegend":{"Name":"Legend","Enabled":{"Source":"ShowLegend"},"Plot":{"Source":"PieChart"}}}}}

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

int main(int argc, char *argv[]){    (void)argc;    (void)argv;    Frenchie::Application::App::push_layer<Frenchie::Application::ImmediateUserInterfaceModelViewControllerLayer>(        "assets/views/FrenchieImmediateUserInterfaceMainWindowView.json",        nullptr);    return Frenchie::Application::App::execute();}

Получим следующее:

Пример парсинга простого файла с верстой без привязки к нему данных

Пример парсинга простого файла с верстой без привязки к нему данных

На картинке выше, слева у нас область с графиком, справа — область под легенду, сверху — радио-кнопка для включения/выключения легенды. При этом видно, что легенда и графики пустые. Они пустые, потому-что мы указали источники данных, по пути к которым в модели данных ничего нет. Чтобы что-то появилось, нужно написать логику. Чтобы, написать логику, нам нужно отнаследоваться от интерфейса контроллера и переопределить его функции:

#include <FrenchieImmediateUserInterfaceModelViewControllerLayer.hpp>class FrenchieImmediateUserInterfacePlotPieController : public Frenchie::Application::ImmediateUserInterfaceViewController{public:    virtual ~FrenchieImmediateUserInterfacePlotPieController(){}    virtual bool setup(std::shared_ptr<Frenchie::Application::ImmediateUserInterfaceViewModel>& _Model) override    {        for (int i = 0; i < 5; i++)        {            m_PieNames.push_back(Frenchie::Core::String::format("Sector-%d", i));            m_PieValues.push_back(5.f * (i + 1));            m_PieColors.push_back(gs_color_rgba(gs_pseudo_random<int>(0, 255), gs_pseudo_random<int>(0, 255), gs_pseudo_random<int>(0, 255), 255));        }        _Model->request<std::string*>("PieNames") = m_PieNames.data();        _Model->request<float*>("PieValues")      = m_PieValues.data();        _Model->request<gs_color*>("PieColors")   = m_PieColors.data();        _Model->request<int>("PieCount")          = m_PieNames.size();        _Model->request<bool>("ShowLegend")       = true;        return true;    }    virtual void update(std::shared_ptr<Frenchie::Application::ImmediateUserInterfaceViewModel>& _Model) override    {    }    virtual void destroy(std::shared_ptr<Frenchie::Application::ImmediateUserInterfaceViewModel>& _Model) override    {    }    std::vector<std::string> m_PieNames  {std::vector<std::string>()};    std::vector<float>       m_PieValues {std::vector<float>()      };    std::vector<gs_color>    m_PieColors {std::vector<gs_color>()   };};

Теперь, отдав модулю декларативного рисования интерфейса файл верстки + контроллер, получим такую картину:

Пример работы графического интерфейса после передачи модулю декларативной рисовки файла с версткой и кастомного контроллера, пробрасывающего данные в элементы графического интерфейса

Пример работы графического интерфейса после передачи модулю декларативной рисовки файла с версткой и кастомного контроллера, пробрасывающего данные в элементы графического интерфейса

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

Пример реагирования модуля декларативной рисовки интерфейса на изменения в переданном ему файле верстки

Пример реагирования модуля декларативной рисовки интерфейса на изменения в переданном ему файле верстки

Выше приведен пример верстки из JSON формата, но, модуль декларативной рисовки интерфейса использует универсальный DOM-парсер, который умеет работать еще и с XML, поэтому, если кому-то удобно, можно верстать по аналогичным правилам в XML.

Чем Frenchie лучше, а чем хуже того же ImGUI ?

Достаточно сложно, на самом деле, быть объективным по отношению к своей программе в части ее сравнения с другими похожими программами, но, я все-таки постараюсь.

Как было сказано ранее, одна из основных целей Frenchie — быть простым. Поэтому, количество знаний, которое требуется для работы с Frenchie, значительно меньше, чем то, что нужно для работы с ImGUI. При этом, ImGUI, не смотря на относительную сложность запуска, обладает очень высокой гибкостью в части интеграции в уже готовые rendering pipeline-ы. Что имеется в виду? Допустим, вы эксперт в компьютерной графике, у вас уже есть готовый 2D/3D renderer и вам нужен инструмент для визуализации того, что он рисует. В таком случае, ImGUI — это то, что вам нужно. Можно ли для таких же целей использовать Frenchie?

Технически, интеграция Frenchie с уже готовым rendering pipe line-ом возможна, но, в таком случае, порог входа в библиотеку резко повысится, т.к вам придется писать свою кастомную реализацию platform и rendering backend-а для интеграции с вашим конкретным rendering pipe line-ом. Хотя, я лично, чисто для себя пока не упел понять, что быстрее: написать кастомные backend-ы для Frenchie или попытаться интегрировать уже готовые backend-ы ImGUI в свой renderer. Пожалуй, лично для меня, этот вопрос пока открытый. С этим, наверное, понятно, а что в плане функционала?

В части функционала по рисовке графического интерфейса, Frenchie имеет практически все тоже самое, что и ImGUI, за исключением небольшого количества элементов. Например, на данный момент у Frenchie нет tab widget-ов. Почему? Дело в том, что я эти виджеты хотел сделать на базе окон, которые уже умеют крепиться друг к другу вкладками, но, руки пока до этого не дошли. А что с 2D renderning-ом, ради которого, собственно говоря, все и делалось?

В части кросс-платформенного 2D rendering-а Frenchie однозначно обгоняет ImGUI, т.к в нем есть возможность рисовать практически любые графические примитивы, натягивать на них текстуры, заливать абсолютно любым цветом (включая градиенты), а также вручную управлять глубиной расположения рисуемых объектов. Однако, как, наверное, заметили читатели набор поддерживаемых графических API во Frenchie довольно скромный по сравнению с ImGUI, но, я постараюсь это все наверстать со временем.

Таким образом, делаем следующий вывод: если, вы эксперт в компьютерной графике и вам требуются инструменты визуализации для вашего 2D/3D renderer-а, то, наверное, лучше взять ImGUI. Если вам нужен компактный кросс-платформенный инструмент с низким порогом вхождения, который умеет рисовать 2D графику и графический интерфейс, то Frenchie — это то, что вам нужно. Также, хотелось бы отметить, что проект Frenchie очень молодой, он активно дорабатывается и поэтому, надеюсь, со временем получится доделать весь недостающий функционал и исправить в нем все ошибки.

Что дальше ?

Как я уже написал проект Frenchie очень молодой, поэтому, впереди предстоит огромная работа по исправлению ошибок, доработке же существующего функционала и разработке нового. На данный момент, ведется работа по оптимизации 2D rendering-а, а также исправлению ошибок. Но, в будущем хотелось бы поддержать больше графических API, а также реализовать модули эффективного рисования 2D и 3D сцен. Надеюсь, было интересно, постараюсь подготовить еще больше интересных материалов.

ссылка на оригинал статьи https://habr.com/ru/articles/1080500/