В данной статье я постараюсь рассказать о фреймворке SpaceVIL (Space of Visual Items Layout), который служит для построения пользовательских графических интерфейсов на платформах .Net / .Net Core и JVM. SpaceVIL является кроссплатформенным и мультиязычным фреймворком, в его основе лежит графическая технология OpenGL, а за создание окон отвечает библиотека GLFW. Используя данный фреймворк, вы можете работать и создавать графические клиентские приложения в операционных системах Linux, Mac OS X, Windows. Для программистов C# в данное время это особенно актуально, учитывая, что Microsoft не собирается переносить WPF на другие ОС и Avalonia является единственным возможным аналогом. Особенностью же SpaceVIL в этом конкретном случае является мультиязычность, то есть на данный момент фреймворк под .Net Core можно использовать в связке со следующими языками программирования: C#, VisualBasic. Фреймворк под JVM можно использовать в связке с языками Java и Scala. То есть, SpaceVIL можно использовать с любым из этих языков и итоговый код будет выглядеть одинаково, поэтому при переходе на другой язык переучиваться заново не придется. SpaceVIL пока находится на стадии альфы, но, несмотря на это, фреймворк можно полноценно использовать уже сейчас, так как во фреймворке есть все необходимое для построения как сложного UI, так и для создания совершенно новых визуальных пользовательских элементов. Цель данной статьи как раз в том, чтобы убедить вас в этом. SpaceVIL разрабатывался с нуля и именно поэтому во фреймворке заложены свои собственные принципы, отличающие его от аналогов. Теперь посмотрим, на что способен фреймворк текущей версии. Теперь давайте рассмотрим структуру элементов. Во фреймворке присутствуют следующие типы элементов: окна, как активные так и диалоговые, контейнеры, для удобного позиционирования элементов, интерактивные элементы и примитивы. Давайте пройдемся кратко по всему вышеперечисленному. Надеюсь, что такое окна объяснять не надо, отмечу лишь, что диалоговое окно блокирует вызвавшее его окно до его закрытия. В SpaceVIL представлены следующие типы контейнеров: Элементы такого типа принимают множество состояний и имеют различные события. Если проще то, все с чем можно взаимодействовать и есть интерактивные элементы, например: кнопка, чекбокс, элемент для ввод текста и прочие. В противовес интерактивным элементам существуют примитивы, совершенно не интерактивные элементы, с ними невозможно контактировать, они существуют просто для того, чтобы показать себя. Типы примитивов: треугольник, прямоугольник, эллипс и чуть посложнее — произвольная фигура, которая может принять облик любой сложности. Так же присутствуют статические классы-сервисы. Некоторые из таких классов позволяют программисту получать и настраивать элементы по своему вкусу. Например, есть класс управляющий стилями элементов, и класс, с помощью которого можно устанавливать произвольную форму отображения интерактивному элементу, класс дефолтных установок и так далее. У меня небольшой пунктик насчет примеров. Всегда можно придумать абстрактный и точно отображающий функционал пример, но если читать его со стороны, то он всегда выглядит очень неубедительно, поэтому в качестве примеров я стараюсь делать маленькие, но готовые приложения, которые служат какой-то вразумительной цели. Давайте перейдем к описанию приложения. Программа представляет собой редактор карточек героев для игр типа «Подземелья и Драконы» и имеет название CharacterEditor. Программа случайным образом генерирует указанное количество различных персонажей с именами, возрастом, расой, полом, классом и характеристиками. Пользователю предоставляется возможность написать биографию и дать персонажу специализированные навыки. В итоге можно сохранить карточку героя в виде текстового файла. Давайте приступим непосредственно к разбору кода. Программа написана на C#. При использовании Java, код будет по сути таким же. В итоге у нас получится такое вот приложение:  На этом этапе мы создадим окно. Напомню, что SpaceVIL использует GLFW, поэтому, если вы пишите приложение для платформы .Net, то скомпилированную библиотеку GLFW необходимо скопировать рядом с исполняемым файлом. В JVM используется враппер библиотеки GLFW (LWJGL), который в своем составе уже имеет скомпилированную GLFW. Далее, наполним область рендеринга необходимыми элементами и придадим им приятный внешний вид. Основные этапы для достижения этого следующие: Все, на этом этапе можно запустить приложение и проверить все ли работает. Для реализации приложения CharacterEditor я решил поместить на окно титульную панель, панель с инструментами и вертикальный разделитель. На панели инструментов будут располагаться: кнопка обновления списка сгенерированных заново персонажей, кнопка сохранения персонажа и элемент с количеством генерируемых персонажей. В левой части вертикального разделителя будет находится список сгенерированных персонажей, а в правой текстовая область для редактирования выбранного из списка персонажа. Чтобы не захламлять класс окна настройками элементов, можно написать статический класс, который предоставит нам готовые по внешнему виду и настройкам элементы. При добавлении важно помнить, что каждый интерактивный элемент, будь то кнопка или список является контейнером, то есть в кнопку можно поместить все что угодно, от примитивов, до другого контейнера и любой сложный элемент это всего лишь набор более простых элементов, которые в сумме служат одной цели. Зная это, необходимо запомнить первое строгое правило — прежде чем добавлять в элемент другие элементы, его самого нужно добавить куда нибудь, либо в сам класс окна (в нашем случае это MainWindow), либо в контейнер или любой другой интерактивный элемент. Давайте поясню на примере: Правильно же будет сначала добавить frame в окно, а потом уже во frame добавить кнопку. Может показаться, что правило очень неудобное и при создании сложного окна придется попотеть, именно поэтому SpaceVIL поощряет создавать свои собственные элементы, которые сильно упрощают добавление элементов. Создание собственных элементов покажу и объясню чуть позже. Давайте вернемся к приложению. Вот такое окно получилось в итоге:  Теперь разберем код: В классе ItemFactory я описал внешний вид и расположение элементов. Например метод ItemFactory.GetToolbarButton() выглядит так: Остальные элементы описаны аналогично. Как вы могли заметить, я применил стиль для элемента ItemText. Поэтому давайте рассмотрим как создавать стили и, сперва, взглянем на код стиля: Как вы видите, я взял уже готовый стиль из класса SpaceVIL.Style для этого элемента и чуть-чуть изменил его, подправив цвета. Каждый стиль может содержать несколько внутренних стилей для стайлинга каждого составляющего сложного элемента. Например, элемент CheckBox состоит из контейнера, индикатора и текста, поэтому у его стиля есть внутренние стили для индикатора ("indicator") и текста ("textline"). Класс Style покрывает все визуальные свойства элементов и в дополнение к этому с помощью стиля можно интерактивно менять форму элемента, например, с эллипса на прямоугольник и обратно. Чтобы применить стиль нужно вызвать у элемента метод SetStyle(Style style), как уже было показано выше: Теперь перейдем к созданию элемента. Сам элемент необязательно должен быть чем-то конкретным, это может быть обычный стек в который вы добавите несколько других элементов. Например, в примере выше у меня есть панель инструментов, в которой три элемента. Сама панель инструментов это просто горизонтальный стек. Все можно было бы оформить в виде отдельного элемента и назвать его ToolBar. Сам по себе он ничего не делает, зато в классе MainWindow сократилось бы количество строк и понимание разметки было бы еще проще, к тому же это еще и способ ослабить первое строгое правило, хотя, конечно, в итоге все равно все подчиняется ему. Ладно, панель инструментов мы больше не трогаем. Нам нужен элемент для списка, который будет отображать сгенерированного персонажа. Чтобы было интереснее, определим состав элемента посложнее: Чтобы создать класс собственного элемента нужно унаследовать его от любого интерактивного элемента из SpaceVIL, если нам подходит хоть какой-нибудь класс в его составе, но для текущего примера мы соберем элемент с нуля, поэтому унаследуем его от базового абстрактного класса интерактивных элементов — SpaceVIL.Prototype. Так же нам нужно реализовать метод InitElements(), в котором мы опишем внешний вид элемента, расположение и вид вложенных элементов, а так же порядок добавления вложенных элементов. Сам элемент назовем CharacterCard. Давайте перейдем к разбору кода готового элемента: В примере я использовал вспомогательный класс, в котором содержатся все базовые характеристики персонажа такие как имя, фамилия, раса, пол, возраст, класс специализации, характеристики, умения и биография. В этом классе все параметры, кроме умений и биографии (предполагается, что пользователь придумает их самостоятельно), сгенерированы. В предыдущем примере было описано два типа событий: MouseHover и MouseClick. Базовых событий на текущий момент всего 11, вот список: Сложные элементы имеют свои уникальные события, но вышеперечисленные события доступны (с оговорками) всем. Синтаксис обработки событий тривиален и выглядит так: Теперь перейдем к фильтрации событий. По умолчанию события проходят сквозь пирамиду элементов. В нашем примере событие нажатия кнопкой мыши на кнопку infoBtn сначала получит сама кнопка, потом это событие получит элемент CharacterCard, далее ListBox, в котором он будет находится, потом SplitArea, VerticalStack и в конце дойдет до базового элемента WСontainer. На каждом элементе можно обработать событие EventMouseClick и все эти действия в указанном порядке будут выполнены, но что если при нажатии на какой-нибудь элемент мы не хотим, чтобы это событие прошло дальше по цепочке? Для этого как раз и есть фильтрация событий. Давайте нагляднее для примера покажу на элементе CharacterCard. Представьте, что в CharacterCard описано событие EventMouseClick, которое в текстовое поле для редактирования персонажа вставляет в текстовом виде информацию из привязанного CharacterInfo. Такое поведение будет логично — мы нажимаем на элемент и видим все параметры персонажа. Далее мы редактируем персонажа, придумывая биографию и умения, либо изменяя характеристики. В какой то момент мы захотели посмотреть краткую информацию о еще одном сгенерированном персонаже из списка и нажимаем на кнопку infoBtn. Если мы не отфильтруем события, то после вызова всплывающей подсказки выполнится EventMouseClick на самом элементе CharacterCard, которое, как мы помним, вставляет текст в поле для редактирования персонажа, что приведет к потере изменений, если мы не сохраним результаты, да и само поведение приложения будет выглядеть нелогично. Поэтому, чтобы событие выполнилось только на кнопке мы можем установить фильтр используя метод infoBtn.SetPassEvents(false). Если вызвать этот метод таким образом, кнопка перестанет пропускать любые события после себя. Допустим мы не хотим пропускать события только щелчков мыши, тогда можно было бы вызвать метод с другими параметрами, например, infoBtn.SetPassEvents(false, InputEventType.MousePress, MouseRelease). Таким образом можно фильтровать события на каждом шаге достигая нужного результата. Можно еще раз посмотреть на приложение, которое получилось в итоге. Конечно же, тут опускаются детали реализации бизнес-логики, в частности, генерация персонажей, их навыков и многое другое, что уже не относится напрямую к SpaceVIL. На полный код приложения можно посмотреть по ссылке на GitHub, где есть уже несколько других примеров по работе со SpaceVIL, как на C#, так и на Java.  В заключение хотелось бы напомнить, что фреймворк находится в активной разработке, так что возможны сбои и падения, так же некоторые возможности могут быть пересмотрены и конечный результат использования может кардинально отличаться от текущего, поэтому, если текущий вариант фреймворка вас устраивает, то не забудьте сделать бэкап этой версии, потому как не могу гарантировать, что новые версии будут обратно совместимыми. Некоторые моменты могут быть переделаны для повышения комфорта и быстроты использования SpaceVIL и пока что не хочется тащить за собой старые и отброшенные идеи. Так же работа SpaceVIL не тестировалась на видеокартах от AMD, из-за отсутствия соответствующего оборудования. Тестирование проводилось на видеокартах от Intel и NVidia. Дальнейшее развитие SpaceVIL будет сконцентрировано на добавлении нового функционала (например, в данный момент нет поддержки градиентов) и оптимизации. Так же хотелось бы упомянуть об ограничении, которое стоит помнить при написании кроссплатформенного приложения с использованием данной технологии — не рекомендуется использовать диалоговые окна (и вообще создавать мультиоконные приложения в ОС Linux из-за ошибок рендеринга), диалоговые окна можно с легкостью заменить диалоговыми элементами. Mac OS X же вообще запрещает создавать мультиоконные приложения, ибо требует, чтобы GUI был запущен только в главном потоке приложения. Фреймворк нужной версии и все представленные примеры тестовых программ вы можете скачать по следующим ссылкам. Первая версия документации доступна так же по ссылке. Напоследок еще немного визуальной демонстрации. Ниже представлены приложения, которые написаны на технологии SpaceVIL, что может дать вам некоторое представление, чего можно добиться используя SpaceVIL.      Программист