SpaceVIL — кроссплатфоремнный GUI фреймворк для разработки на .Net Core, .Net Standard и JVM +33


В данной статье я постараюсь рассказать о фреймворке 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 полный контроль над происходящим.
  • Любое приложение, написанное на SpaceVIL, будет выглядеть абсолютно одинаково на всех платформах. Нет никаких подводных камней. Можно использовать любую версию SpaceVIL (.Net / JVM, Mаc OS X, Linux, Windows), результат и внешний вид всегда будет один и тот же.
  • SpaceVIL версии для JVM идентичен по использованию SpaceVIL версии для .Net
  • SpaceVIL предоставляет возможности для глубокой кастомизации элемента, так как все интерактивные объекты являются контейнерами для других интерактивных объектов.
  • Фреймворк очень гибок и прост в использовании, так как базовых строгих правил в нем немного, а единственное, что нужно понимать перед началом работы с ним – это что означают параметры Padding, Margin, Alignment (а их знает любой, кто создавал простенькие интерфейсы, например, в WPF, Android Studio или писал стили в CSS).
  • SpaceVIL не потребует от вас какого-либо глубокого изучения его внутренностей и он будет выполнять именно то, что вы напишите. Все элементы подчиняются общим правилам, один подход будет работать на всех элементах. Запомнив основу, можно будет предугадывать как состав элементов, так и способы его стайлинга.
  • Фреймворк очень легковесный, меньше мегабайта и все в одном файле.

Возможности


Теперь посмотрим, на что способен фреймворк текущей версии.


  • Для использования доступно 54 элемента, из которых 10 – это специализированные контейнеры, 6 примитивов (не интерактивных элементов) и 38 интерактивных элементов различного назначения.
  • На основе всех этих элементов, вкупе с реализацией специальных интерфейсов, можно создавать свои собственные элементы любой сложности.
  • Присутствует стайлинг элементов, доступны возможности по созданию целых тем стилей или изменение/замена стиля любого стандартного элемента во фреймворке. Пока в SpaceVIL присутствует только одна тема и она установлена по умолчанию.
  • Присутствует система состояний. Каждому элементу можно назначить визуальное состояние на один из способов внешнего воздействия: наведение курсора на элемент, нажатие кнопки мыши, отпускание кнопки мыши, переключение, фокусировка и выключение элемента.
  • Присутствует фильтрация событий. Каждый элемент при взаимодействии может отфильтровывать проходящие сквозь него события, что позволяет одним событиям проходить сквозь элемент, а другим отбрасываться. В примере расскажу об этом подробнее.
  • Реализована система плавающих независимых элементов.
  • Реализована система диалоговых окон и диалоговых элементов.
  • Реализован независимый рендеринг. Каждое окно имеет два потока – один управляет рендерингом, другой выполнением задач от приходящих событий, то есть окно теперь всегда продолжает рендеринг (и не "висит"), независимо от задачи, которая была запущена после нажатия какой-нибудь кнопки.

Структура


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


Контейнеры


В SpaceVIL представлены следующие типы контейнеров:


  • Общий контейнер (элементы внутри такого контейнера позиционируются за счет параметров alignment, padding, margin, size и size policy).
  • Вертикальный и горизонтальный стеки (элементы, добавленные в такой контейнер, будут располагаться по порядку без необходимости точной настройки параметров, которая нужна при использовании предыдущего типа контейнера).
  • Сетка (Grid) – элементы добавляются в ячейки сетки и позиционируются внутри своей ячейки.
  • Список (ListBox, TreeView), контейнер на основе вертикального стека, но с возможностью прокрутки для отображения элементов, которые не вместились в контейнер.
  • Разделитель (SplitArea), контейнер может быть двух типов – вертикальный и горизонтальный, разделяет две области и интерактивно управляет размерами этих областей.
  • Контейнер со вкладками (TabView), управляет видимостью страниц.
  • WrapGrid, позиционирует элементы внутри ячеек определенного размера, заполняет все свободное пространство согласно ориентации с возможностью прокрутки (самый яркий пример – проводник в Windows в режиме отображения иконок).
  • И, наконец, свободный контейнер, наверное самый редкий контейнер из названных, представляет собой бесконечную область, на которую можно добавлять любые элементы фиксированного размера, но лучше использовать в сочетании с элементом типа ResizableItem.

Интерактивные элементы


Элементы такого типа принимают множество состояний и имеют различные события. Если проще то, все с чем можно взаимодействовать и есть интерактивные элементы, например: кнопка, чекбокс, элемент для ввод текста и прочие.


Примитивы


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


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


Пример простого приложения с использованием фреймворка SpaceVIL


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


Давайте перейдем к описанию приложения. Программа представляет собой редактор карточек героев для игр типа «Подземелья и Драконы» и имеет название CharacterEditor. Программа случайным образом генерирует указанное количество различных персонажей с именами, возрастом, расой, полом, классом и характеристиками. Пользователю предоставляется возможность написать биографию и дать персонажу специализированные навыки. В итоге можно сохранить карточку героя в виде текстового файла. Давайте приступим непосредственно к разбору кода. Программа написана на C#. При использовании Java, код будет по сути таким же.


В итоге у нас получится такое вот приложение:



Создание окна приложения


На этом этапе мы создадим окно. Напомню, что SpaceVIL использует GLFW, поэтому, если вы пишите приложение для платформы .Net, то скомпилированную библиотеку GLFW необходимо скопировать рядом с исполняемым файлом. В JVM используется враппер библиотеки GLFW (LWJGL), который в своем составе уже имеет скомпилированную GLFW.


Далее, наполним область рендеринга необходимыми элементами и придадим им приятный внешний вид. Основные этапы для достижения этого следующие:


  • Инициализация SpaceVIL перед его использованием. В функции Main достаточно написать:

if (!SpaceVIL.Common.CommonService.InitSpaceVILComponents())
    return;

  • Теперь создадим окно. Чтобы это сделать нужно написать класс окна, унаследовав его от класса SpaceVIL.ActiveWindow, описать метод InitWindow() и задать ему несколько базовых параметров, таких как название окна, текст титульной панели и размеры. В итоге получим код, который выглядит так:

using System; using SpaceVIL;
namespace CharacterEditor
{
    internal class MainWindow : ActiveWindow
    {
        public override void InitWindow()
        {
            SetParameters("CharacterEditor", "CharacterEditor", 1000, 600);
        }
    }
}

  • Осталось только создать экземпляр этого класса и вызвать его. Для этого дополним метод Main следующими строчками кода:

MainWindow mw = new MainWindow();
mw.Show();

Все, на этом этапе можно запустить приложение и проверить все ли работает.


Наполнение элементами


Для реализации приложения CharacterEditor я решил поместить на окно титульную панель, панель с инструментами и вертикальный разделитель. На панели инструментов будут располагаться: кнопка обновления списка сгенерированных заново персонажей, кнопка сохранения персонажа и элемент с количеством генерируемых персонажей. В левой части вертикального разделителя будет находится список сгенерированных персонажей, а в правой текстовая область для редактирования выбранного из списка персонажа. Чтобы не захламлять класс окна настройками элементов, можно написать статический класс, который предоставит нам готовые по внешнему виду и настройкам элементы. При добавлении важно помнить, что каждый интерактивный элемент, будь то кнопка или список является контейнером, то есть в кнопку можно поместить все что угодно, от примитивов, до другого контейнера и любой сложный элемент это всего лишь набор более простых элементов, которые в сумме служат одной цели. Зная это, необходимо запомнить первое строгое правило — прежде чем добавлять в элемент другие элементы, его самого нужно добавить куда нибудь, либо в сам класс окна (в нашем случае это MainWindow), либо в контейнер или любой другой интерактивный элемент. Давайте поясню на примере:


public override void InitWindow()
{
    SetParameters("CharacterEditor", "CharacterEditor", 1000, 600);
    //создадим простейший контейнер
    Frame frame = new Frame();
    //создадим кнопку, которую будем добавлять в контейнер frame
    ButtonCore btn = new ButtonCore("Button");
    //следующий код нарушает вышеописанное правило,
    //что приведет к рантайм исключению при запуске.
    //Контейнер frame еще никуда не добавлен и,
    //следовательно, не проинициализирован системой.
    frame.AddItem(btn);
    //добавим контейнер frame в наше окно
    AddItem(frame);
}

Правильно же будет сначала добавить frame в окно, а потом уже во frame добавить кнопку. Может показаться, что правило очень неудобное и при создании сложного окна придется попотеть, именно поэтому SpaceVIL поощряет создавать свои собственные элементы, которые сильно упрощают добавление элементов. Создание собственных элементов покажу и объясню чуть позже. Давайте вернемся к приложению. Вот такое окно получилось в итоге:



Теперь разберем код:


Итоговый код разметки MainWindow
internal ListBox ItemList = new ListBox(); //контейнер типа список для персонажей
internal TextArea ItemText = new TextArea(); //область для текстового редактирования персонажа
internal ButtonCore BtnGenerate; //кнопка генерации персонажей
internal ButtonCore BtnSave; //кнопка сохранения выбранного персонажа
internal SpinItem NumberCount; //элемент количества генерируемых персонажей
public override void InitWindow()
{
    SetParameters("CharacterEditor", "CharacterEditor", 1000, 600);
    IsBorderHidden = true; //этот параметр скрывает нативную титульную панель
    IsCentered = true; //наше окно будет появляться в центре экрана
    //титульная панель
    TitleBar title = new TitleBar(nameof(CharacterEditor));
    //установим иконку для титульной панели
    title.SetIcon( DefaultsService.GetDefaultImage(EmbeddedImage.User, 
    EmbeddedImageSize.Size32x32), 20, 20);
    //основной контейнер, в который мы поместим все остальное
    VerticalStack layout = ItemFactory.GetStandardLayout(title.GetHeight());
    //панель инструментов
    HorizontalStack toolbar = ItemFactory.GetToolbar();
    //вертикальный разделитель
    VerticalSplitArea splitArea = ItemFactory.GetSplitArea();
    //кнопка генерации
    BtnGenerate = ItemFactory.GetToolbarButton();
    //кнопка сохранения
    BtnSave = ItemFactory.GetToolbarButton();
    //элемент количества генерируемых персонажей
    NumberCount = ItemFactory.GetSpinItem();
    //устанавливаем стиль текстовому полю
    ItemText.SetStyle(StyleFactory.GetTextAreaStyle());
    //добавление элементов согласно строгому правилу
    AddItems(title, layout);
    layout.AddItems(toolbar, splitArea);
    toolbar.AddItems(BtnGenerate, BtnSave, ItemFactory.GetVerticalDivider(), NumberCount);
    splitArea.AssignLeftItem(ItemList);
    splitArea.AssignRightItem(ItemText);
    //добавим картинки на кнопки
    BtnGenerate.AddItem(ItemFactory.GetToolbarIcon(
        DefaultsService.GetDefaultImage(EmbeddedImage.Refresh,
            EmbeddedImageSize.Size32x32)));
    BtnSave.AddItem(ItemFactory.GetToolbarIcon(
        DefaultsService.GetDefaultImage(EmbeddedImage.Diskette,
            EmbeddedImageSize.Size32x32)));
}

В классе ItemFactory я описал внешний вид и расположение элементов. Например метод ItemFactory.GetToolbarButton() выглядит так:


internal static ButtonCore GetToolbarButton()
{
    ButtonCore btn = new ButtonCore();
    //параметры расположения и внешнего вида
    btn.SetBackground(55, 55, 55); //цвет
    btn.SetHeightPolicy(SizePolicy.Expand); //по высоте кнопка растянется по размеру контейнера
    btn.SetWidth(30); //ширина кнопки
    btn.SetPadding(5, 5, 5, 5); //отступ от краев кнопки для добавляемых элементов
    //добавим состояние, которое меняет цвет кнопки при наведении на нее курсора мышки
    btn.AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(30, 255, 255, 255))); //в классе ItemState указываем цвет
    return btn;
}

Остальные элементы описаны аналогично.


Создание и применение стилей


Как вы могли заметить, я применил стиль для элемента ItemText. Поэтому давайте рассмотрим как создавать стили и, сперва, взглянем на код стиля:


Style style = Style.GetTextAreaStyle(); //беру уже готовый стиль для этого элемента
style.Background = Color.Transparent; // устанавливаю цвет
Style textedit = style.GetInnerStyle("textedit"); //извлекаю внутренний стиль текстового поля
textedit.Foreground = Color.LightGray; //устанавливаю цвет текста
Style cursor = textedit.GetInnerStyle("cursor"); //извлекаю внутренний стиль для курсора
cursor.Background = Color.FromArgb(0, 162, 232);//устанавливаю цвет курсора

Как вы видите, я взял уже готовый стиль из класса SpaceVIL.Style для этого элемента и чуть-чуть изменил его, подправив цвета. Каждый стиль может содержать несколько внутренних стилей для стайлинга каждого составляющего сложного элемента. Например, элемент CheckBox состоит из контейнера, индикатора и текста, поэтому у его стиля есть внутренние стили для индикатора ("indicator") и текста ("textline").


Класс Style покрывает все визуальные свойства элементов и в дополнение к этому с помощью стиля можно интерактивно менять форму элемента, например, с эллипса на прямоугольник и обратно. Чтобы применить стиль нужно вызвать у элемента метод SetStyle(Style style), как уже было показано выше:


ItemText.SetStyle(StyleFactory.GetTextAreaStyle());

Создание собственного элемента


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


Чтобы было интереснее, определим состав элемента посложнее:


  • Иконка персонажа, цвет которой указывает на принадлежность к фэнтезийной расе.
  • Имя, фамилия и раса персонажа в текстовом виде.
  • Кнопка быстрой подсказки по персонажу (нужна для демонстрации фильтрации событий).
  • Кнопка удаления персонажа, если он нам не подходит.

Чтобы создать класс собственного элемента нужно унаследовать его от любого интерактивного элемента из SpaceVIL, если нам подходит хоть какой-нибудь класс в его составе, но для текущего примера мы соберем элемент с нуля, поэтому унаследуем его от базового абстрактного класса интерактивных элементов — SpaceVIL.Prototype. Так же нам нужно реализовать метод InitElements(), в котором мы опишем внешний вид элемента, расположение и вид вложенных элементов, а так же порядок добавления вложенных элементов. Сам элемент назовем CharacterCard.


Давайте перейдем к разбору кода готового элемента:


Код элемента CharacterCard
using System;
using System.Drawing;
using SpaceVIL;
using SpaceVIL.Core;
using SpaceVIL.Decorations;
using SpaceVIL.Common;

namespace CharacterEditor
{
    //наследуем класс от SpaceVIL.Prototype
    internal class CharacterCard : Prototype
    {
        private Label _name;
        private CharacterInfo _characterInfo = null;
        //конструктор принимает в качестве параметра класс CharacterInfo,
        //в котором указаны все базовые параметры персонажа
        internal CharacterCard(CharacterInfo info)
        {
            //задаем внешний вид и размеры элемента
            //размер фиксированный по высоте и растягивающийся по ширине
            SetSizePolicy(SizePolicy.Expand, SizePolicy.Fixed);
            SetHeight(30); //высота нашего элемента
            SetBackground(60, 60, 60); //цвет элемента
            SetPadding(10, 0, 5, 0); //отступы для вложенных элементов
            SetMargin(2, 1, 2, 1); //отступы самого элемента
            AddItemState(ItemStateType.Hovered, new ItemState(Color.FromArgb(30, 255, 255, 255)));
            _characterInfo = info; //сохраняем ссылку на информацию о персонаже
            //устанавливаем имя персонажа и расу в Label
            _name = new Label(info.Name + " the " + info.Race);
        }
        public override void InitElements()
        {
            //иконка расы
            ImageItem _race = new ImageItem(DefaultsService.GetDefaultImage(
                EmbeddedImage.User, EmbeddedImageSize.Size32x32), false);
            _race.KeepAspectRatio(true); //сохраняем соотношение сторон
            //ширина ImageItem будет фиксированной
            _race.SetWidthPolicy(SizePolicy.Fixed); 
            _race.SetWidth(20); //ширина ImageItem
            //выравниваем ImageItem слева и по центру по вертикали
            _race.SetAlignment(ItemAlignment.Left, ItemAlignment.VCenter);
            //устанавливаем оверлей (замена) цвета картинки согласно расе
            switch (_characterInfo.Race) 
            {
                case CharacterRace.Human:
                    //синий для людей
                    _race.SetColorOverlay(Color.FromArgb(0, 162, 232)); 
                    break;
                case CharacterRace.Elf:
                    //зеленый для эльфов
                    _race.SetColorOverlay(Color.FromArgb(35, 201, 109)); 
                    break;
                case CharacterRace.Dwarf:
                    //оранжевый для гномов
                    _race.SetColorOverlay(Color.FromArgb(255, 127, 39)); 
                    break;
            }
            //параметры Label _name
            _name.SetMargin(30, 0, 30, 0); //отступ слева и справа
            //параметры кнопки быстрой подсказки
            ButtonCore infoBtn = new ButtonCore("?");
            infoBtn.SetBackground(Color.FromArgb(255, 40, 40, 40));
            infoBtn.SetWidth(20);
            infoBtn.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Expand);
            infoBtn.SetFontStyle(FontStyle.Bold);
            infoBtn.SetForeground(210, 210, 210);
            infoBtn.SetAlignment(ItemAlignment.VCenter, ItemAlignment.Right);
            infoBtn.SetMargin(0, 0, 20, 0);
            infoBtn.AddItemState(ItemStateType.Hovered,
                new ItemState(Color.FromArgb(0, 140, 210)));
            //настройка фильтра событий
            //кнопка info не пропустит после себя ни одного события
            infoBtn.SetPassEvents(false); 
            //установка обработчиков событий
            //при наведении курсора на кнопку info
            //устанавливаем состояние hover на весь элемент
            infoBtn.EventMouseHover += (sender, args) => 
            {
                SetMouseHover(true);
            };
            //при клике мыши на кнопку info вызываем всплывающее
            //окошко с базовой информацией о персонаже
            infoBtn.EventMouseClick += (sender, args) =>
            {
                //иконка расы ImageItem 
                ImageItem race = new ImageItem(DefaultsService.GetDefaultImage(
                    EmbeddedImage.User, EmbeddedImageSize.Size32x32), false);
                race.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Fixed);
                race.SetSize(32, 32);
                race.SetAlignment(ItemAlignment.Left, ItemAlignment.Top);
                race.SetColorOverlay(_race.GetColorOverlay());
                //всплывающее окно
                PopUpMessage popUpInfo = new PopUpMessage(
                    _characterInfo.Name + "\n" +
                    "Age: " + _characterInfo.Age + "\n" +
                    "Sex: " + _characterInfo.Sex + "\n" +
                    "Race: " + _characterInfo.Race + "\n" +
                    "Class: " + _characterInfo.Class);
                //время действия всплывающей подсказки 3 секунды
                popUpInfo.SetTimeOut(3000);
                popUpInfo.SetHeight(200); //высота всплывающего окна
                //отображаем всплывающее окно, в качестве параметра
                //передаем текущий хендлер окна
                popUpInfo.Show(GetHandler());
                //добавим иконку расы на всплывающее окошко
                popUpInfo.AddItem(race);
            };
            //кнопка удаления персонажа
            ButtonCore removeBtn = new ButtonCore();
            removeBtn.SetBackground(Color.FromArgb(255, 40, 40, 40));
            removeBtn.SetSizePolicy(SizePolicy.Fixed, SizePolicy.Fixed);
            removeBtn.SetSize(10, 10);
            removeBtn.SetAlignment(ItemAlignment.VCenter, ItemAlignment.Right);
            removeBtn.SetCustomFigure(new CustomFigure(false,
                GraphicsMathService.GetCross(10, 10, 2, 45)));
            removeBtn.AddItemState(ItemStateType.Hovered,
                new ItemState(Color.FromArgb(200, 95, 97)));
            //опишем событие при нажатии кнопкой мыши на removeBtn
            removeBtn.EventMouseClick += (sender, args) =>
            {
                RemoveSelf(); //удаляем элемент
            };
            //добавляем все созданные элементы в наш CharacterCard
            AddItems(_race, _name, infoBtn, removeBtn);
        }
        internal void RemoveSelf()
        {
            //берем родителя и делаем запрос на удаление самого себя
            GetParent().RemoveItem(this);
        }
        public override String ToString()
        {
            return _characterInfo.ToString();
        }
    }
}

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


Обработка событий и их фильтрация


В предыдущем примере было описано два типа событий: MouseHover и MouseClick. Базовых событий на текущий момент всего 11, вот список:


  • EventMouseHover
  • EventMouseLeave
  • EventMouseClick
  • EventMouseDoubleClick
  • EventMousePress
  • EventMouseDrag
  • EventScrollUp
  • EventScrollDown
  • EventKeyPress
  • EventKeyRelease
  • EventTextInput

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


Синтаксис обработки событий тривиален и выглядит так:


//Для C#
item.EventMouseClick += (sender, args) =>
{
    //делаем что-нибудь
};

//Для Java
item.eventMouseClick.add((sender, args) -> {
    //делаем что-нибудь
});

Теперь перейдем к фильтрации событий. По умолчанию события проходят сквозь пирамиду элементов. В нашем примере событие нажатия кнопкой мыши на кнопку 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.


Скриншот готового приложения CharacterEditor


Заключение


В заключение хотелось бы напомнить, что фреймворк находится в активной разработке, так что возможны сбои и падения, так же некоторые возможности могут быть пересмотрены и конечный результат использования может кардинально отличаться от текущего, поэтому, если текущий вариант фреймворка вас устраивает, то не забудьте сделать бэкап этой версии, потому как не могу гарантировать, что новые версии будут обратно совместимыми. Некоторые моменты могут быть переделаны для повышения комфорта и быстроты использования SpaceVIL и пока что не хочется тащить за собой старые и отброшенные идеи. Так же работа SpaceVIL не тестировалась на видеокартах от AMD, из-за отсутствия соответствующего оборудования. Тестирование проводилось на видеокартах от Intel и NVidia. Дальнейшее развитие SpaceVIL будет сконцентрировано на добавлении нового функционала (например, в данный момент нет поддержки градиентов) и оптимизации.


Так же хотелось бы упомянуть об ограничении, которое стоит помнить при написании кроссплатформенного приложения с использованием данной технологии — не рекомендуется использовать диалоговые окна (и вообще создавать мультиоконные приложения в ОС Linux из-за ошибок рендеринга), диалоговые окна можно с легкостью заменить диалоговыми элементами. Mac OS X же вообще запрещает создавать мультиоконные приложения, ибо требует, чтобы GUI был запущен только в главном потоке приложения.


Фреймворк нужной версии и все представленные примеры тестовых программ вы можете скачать по следующим ссылкам. Первая версия документации доступна так же по ссылке.


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


Скриншоты приложений, написанных с помощью SpaceVIL






Ссылки





К сожалению, не доступен сервер mySQL