В этой статье я расскажу про возможности компактной кросс‑платформенной библиотеки с открытым исходным кодом для разработки приложений с графическим интерфейсом на языке 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();}
Этот код нарисует нам вот такие вот аналоговые часы на мониторе:
Помимо рисования на мониторе, 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-ы и.т.д:
4. Деревья и таблицы. При этом, таблицы имеют возможность выделения видимой части по вертикали и горизонтали, что позволяет эффективно рисовать таблицы по несколько десятков тысяч строк и столбцов без потери производительности. Таблицы также имеют возможность полной кастомизации ячеек с данными, ячеек с названиями строк и столбцов, а также угловой ячейки, т.е мы можем верстать абсолютно любой content внутри ячеек таблицы:
5. 2D графики, векторные и круговые диаграммы:
6. Механика 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/