В данной статье я постараюсь рассказать о фреймворке 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






Ссылки


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


  1. hatman
    20.04.2019 16:06
    +1

    А можно узнать цель фреймворка.

    т.е. если это компания стартап, которой нужно какой-то пульт от своего саса завернуть под десктоп — они юзают электон.

    Если уже надо сделать качественно и без дропов — пишут натив.

    Чем этот фреймворк отличается выгоднее от электрона, который позволяет из говна и палок собрать работающее решение, и есть ли в нем надежность натива?


    1. kekekeks
      20.04.2019 16:23

      По всей видимости оно вполне себе пригодно как встраиваемое решение для игр, благо рендерится самостоятельно средствами OpenGL.


    1. rsedaikin Автор
      20.04.2019 17:35

      Основная идея — это предоставить простую в использовании и кроссплатформенную альтернативу при проектировании десктопного UI для JVM и .Net Core.
      Ну и я понимаю, что просто так и сразу люди не перейдут на эту технологию. Изначально, фреймвок в некоторой степени рассматривался в применении программистами бэк-энда, которые никак не связаны с фронтом, но в силу некоторых обстоятельств (рабочих, либо просто по желаю) захотели некоторый бэк-энд обернуть в GUI, но без потерь во времени и преодоления порога вхождения при изучении любой современной GUI системы. Другими словами целью было создать систему GUI с максимально низким порогом вхождения.


  1. kekekeks
    20.04.2019 16:18
    +1

    1) Что с разметкой? Всё кодом? Если есть нормальная разметка, то есть ли к ней визуальный дизайнер/превьювер?
    2) Как обстоят дела с data binding и прочими MVVM?
    3) Есть ли виртуализация списков?


    Если ничего этого нет, то можно с тем же успехом взять GTK#/QML.NET.


    Далее, у вас указано, что оно базируется на glfw. Как обстоят дела с непрямоугольными векторными элементами интерфейса типа кривых безье? Все векторные иконки сейчас на них.


    1. rsedaikin Автор
      20.04.2019 17:13

      1) Пока все кодом, дизайнера пока нет, но он в планах
      2) Система, похожая на data binding (WPF) сейчас в разработке, проектируется наиудобнейший подход, но пока до релиза далеко
      3) То же самое как во втором пункте

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


      1. kekekeks
        20.04.2019 17:36
        +2

        Будете заниматься разметкой — гляньте на https://github.com/kekekeks/XamlIl Мы его сейчас вкручиваем для компиляции XAML-а билд-таской в MSIL. В итоге после сборки в отладчике это выглядит примерно вот так:


        Скрытый текст


        1. rsedaikin Автор
          20.04.2019 17:45

          Ок. Спасибо, стоит попробовать.


      1. kekekeks
        20.04.2019 17:39

        но нужно ее предоставить в триангулированном виде

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


  1. kekekeks
    20.04.2019 16:24

    Mac OS X же вообще запрещает создавать мультиоконные приложения, ибо требует, чтобы GUI был запущен только в главном потоке приложения.

    Я не знаю, что именно у вас там за ограничения, но мы вообще успешно рисуем в несколько окон с одного NSOpenGLContext. Есть подозрение, что вы пытаетесь использовать OpenGL на виртуалке, в таком случае случаются знатные спецэффекты.


    1. rsedaikin Автор
      20.04.2019 17:20

      Вы окна создаете с использованием glfw?


      1. kekekeks
        20.04.2019 17:31

        Нет, с использованием NSWindow, как предписывает эпловская документация.


        1. rsedaikin Автор
          20.04.2019 17:42

          Понятно. Mac OS X не позволяет при использовании glfw создавать за раз больше одного окна. Естественно, я сейчас тоже все чаще думаю о том, чтобы написать свой мини-аналог glfw, чтобы использовать необходимый потенциал разных ОС, но в данный момент я пока не могу отказаться от glfw.


  1. KpoKec
    20.04.2019 18:16

    А IDE какое? Своё или интегрируется в студию? А когда появится визуальный редактор форм — то где он будет?


    1. rsedaikin Автор
      20.04.2019 18:23

      IDE любое на ваш выбор, использование SpaceVIL не отличается от использования любой другой библиотеки, поэтому неограничен в выборе IDE.
      По поводу редактора. Первая версия планируется в виде отдельного приложения (наподобие JavaFX Scene Builder), которое будет синхронизироваться с открытым проектом в любой выбранной IDE.


  1. DieselMachine
    20.04.2019 18:38

    Интересно. Код проекта закрыт?


    1. rsedaikin Автор
      20.04.2019 18:51

      Ну, код на Java и .Net успешно декомпилируется.


  1. cyber_roach
    21.04.2019 01:17
    +1

    Здравствуйте.
    Я так понял мне, как интегратору UI в приложения еще один стандарт разметки изучать?
    Какие гарантии что вы не развалитесь через год и все мои знания не превратятся в пыль?
    Вон Flutter на носу, его бы осилить.
    У товарища kekekeks в этом плане более перспективнее всё. XAML это и платформа MS и Xamarin и Noises под Unity 3D/unreal. В случае если авалонию забросят, знания и опыт, которые я копил годами найдут нишу применения. Хотя, пользуясь случаем, поругаю и авалонию, за придумывание велосипедов а-ля кастомные биндинги и проперти, отказ от авалонии в одном из проектов был как раз по причине что не WPF like код, слишком много времени нужно потратить на портацию, проект большой, было предложение «давайте попробуем, сколько стоит?»
    Сейчас, мне самой перспективным под кросплатформенный .net видится связка Unity3D + Noesis GUI. На нем я с толкача несколько старых WPF UI завел вообще без проблем.
    Noesis это шустрый привычный XAML, а Unity3D решение всех проблем с кросплатформенностью и производительностью. Производительность это важно. Добавляйте в свои GUI фреймворки примеры, где в списках по миллиону записей, где шарики на фоне генерируются анимациями, летают и после пролета уничтожаются, чтобы я смотрел на загрузку оперативной памяти/процессора и ЗАХОТЕЛ пользоваться фрейморком для GUI.
    В свое время когда с WPF портировал один умный дом на UWP (кстати, всего месяц убил) я просто визжал от счастья, насколько быстро там все летало. «Наконец-то полноценный мультитач, плавный зум и шустрые анимации» — думал я.
    А без этого вообще не понятно «А вдруг не потянет?» Тратить 3-5 месяцев работ на энтузиазме? времена не те. Глядя на простецкий пример в посте, мне кажется это шагом назад.
    Noesis сначала тоже шли по пути «а придумаем ка мы свой WPF с блекджеком и сами знаете кем» 3 года назад я посмотрел на них и бросил, но сейчас, на поводу у сообщества они идут праведным путем, переписывая все под WPF like, чтобы такие как я могли максимально быстро перейти на платформу. (хотя UWP like мне нравился бы еще больше) У них даже сам UI отдельно можно в Blend как WPF проект править и запускать Win приложением на тесты, ИМХО — супер. Лучше и быть не может.
    Пока жду что там по .netCore у Unity3D, мне кажется т.к. они смогли перейти на .net framework 4, отказались от mono develop, все предпосылки к этому есть. Также жду, когда WPF выложат в OpenSource полностью, чтобы Noesis закрыла то, с чем у них проблемы (бихейворы например) не критично, но полный WPF like + .net4/core даст им преимущество вида «а не попробовать ли мне быстренько портировать вот этот телериковский контрольчик». Розовые мечты конечно, но 2020й год покажет. В этой связке другие проблемы беспокоят, которые уже упоминали в комментариях. MVVM, виртуализация, связка с OS (нотификации там всякие из трея) т.е. не сам GUI, а что сразу за ним. Посему на авалонии крест даже и не думал пока ставить, в отличии от того же Xamarin.
    P/S Не нравится XAML — есть армия верстальщиков на HTML.


    1. rsedaikin Автор
      21.04.2019 09:31

      Здравствуйте.
      Я вас прекрасно понимаю, так как сам не раз оказывался в ситуации, когда выбор стека технологий не являлся очевидным для проекта и некоторые его модули в последствии были переписаны полностью, ибо выбранная технология для них безнадежно устарела и заброшена, либо была перспективной, но так же заброшенной. Так же случалось и со знаниями, знания и опыт есть, а применить негде.
      Пока вам предлагаю просто последить за SpaceVIL. В ближайшее время я проект забрасывать не собираюсь, ибо он еще не готов, а я не люблю оставлять проекты незавершенными.


    1. kekekeks
      21.04.2019 09:46

      не WPF like код, слишком много времени нужно потратить на портацию

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


      1. cyber_roach
        21.04.2019 11:12
        +1

        Круть! Сделайте анонс на хабре обязательно, я так понимаю это тоже уже ближе к 2020 будет, т.к. MS там что-то совсем не шевелятся с выкладкой на GitHub.


      1. dimaaan
        21.04.2019 13:00

        У нас сейчас есть план дождаться полной выкладки WPF в опенсорс

        а это разве не полная версия?


        1. T-D-K
          21.04.2019 13:28

          По ссылке есть такие слова:
          «We have published only a small part of the WPF source. We will continue to publish WPF components as part of the .NET Core 3 project. We will publish source and tests at the same time for each component.»
          Вот RoadMap: github.com/dotnet/wpf/blob/master/roadmap.md


  1. easty
    21.04.2019 08:40

    А лицензия?


    1. rsedaikin Автор
      21.04.2019 09:35

      Лицензия свободная. Используйте в любых целях, в том числе и коммерческих, но, как обычно, без гарантий «As Is».


  1. 10E137
    21.04.2019 11:38

    Крайне интересное начало, но под конец както стало не очень интересно. Нету ссылок ни на исходные коды, да и никаких Nuget-ов не завезли: надо както руками добавлть референсы, да еще и бинарники в репозиторий тащить (как самой библиотеки, так и зависимостей). Печаль.


    1. rsedaikin Автор
      21.04.2019 11:41

      В nuget со временем скорее всего появится