Разработка графического кроссплатформенного приложения на C#. LXUI

Всем привет! Меня зовут Леонид, я являюсь разработчиком программного обеспечения (ПО) на языках программирования С++, C# и Java. Основной род деятельности за последние 10 лет создание систем сбора данных, мониторинга и автоматизации в промышленной сфере, так же участвовал в разработке ПО для финансового сектора. В одном из проектов была поставлена задача разработать пользовательское ПО, которое должно работать в операционных системах Windows, Linux и Android, используя среду .NET и оно не должно было быть WEB приложением (так как боялись за низкую производительность, да и в целом подобные SCADA системы в основном развивались в десктопном исполнении), скорее всего это была архитектурная ошибка, но благодаря ей появилась основа для нового компактного фреймворка. Забегая вперед, скажу это была достаточно амбициозная идея, так как на дворе был 2017 год и необходимых кроссплатформенных систем с пользовательским интерфейсом на .NET не существовало.

Для Windows систем был устаревший Windows Forms и все больше набирал обороты WPF, под Android был только Xamarin, который казался жутко тормозным и глючным в плане разработки, а под Linux все было совсем плохо. Имея достаточно неплохой опыт в создании пользовательских элементов управления еще со времен C++ Builder, было решено сделать свой UI с блэкджеком и шарпом.

Первые пробы

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

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

  2. Получать информацию с устройств ввода (клавиатура, мышь, тач-панель и т.п.)

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

  4. Делать это все максимально быстро, иначе наш интерфейс будет медленным и неудобным в использовании:)

В качестве основы будущего фреймворка LXUI был взят кроссплатформенный игровой движок MonoGame, который в свою очередь продолжал традиции XNA от Microsoft. MonoGame уже включает практически все необходимое из списка, остается только сделать надстройку над ним. На самом деле это большая и сложная работа, построить быстрый и надежный интерфейс, особенно если необходимо обрабатывать тысячи элементов управления. Даже просто вывести текст на экране с возможностью его выравнивания, переноса, обрезки, вывода большого объема текста (10ки мегабайт), потокобезопасным и чтоб при этом интерфейс был отзывчивым, требуется немало усилий и различных трюков оптимизации.

Так появился первый рабочий проект «Omega» и дальнейшие его ответвление «Trassa».

СПО СПО «Трасса»СПО СПО «Трасса»СПО СПО «Трасса»

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

SDL2 и Material Design

В какой-то момент времени я наткнулся на замечательную компактную библиотеку SDL2, это свободно-распространяемый кроссплатформенный игровой движок с огромными встроенными возможностями и очень высокой производительностью. После внимательного изучения ее функционала было принято решение написать новую версию LXUI с 0, используя данный движок как основу. На самом деле рассматривались и другие движки, в том числе и Unity, но все они не подходили в силу большого размера исполняемых файлов и поддержкой платформ, например SDL2 по прежнему поддерживает Windows XP, кто-то скажет, что это бред и данная операционная система давно устарела, но к сожалению это не так, в государственных и финансовых структурах до сих пор иногда встречается. Задача была сделать библиотеку максимально компактной, а не монстра в сотни мегабайт для пустого проекта, и так же с максимальной поддержкой даже уже устаревших операционных систем. Поэтому LXUI написана на фреймворке .NET 4.0 (также есть адаптация под .NET Standard 2.0) и позволяет использовать устаревшие среды разработки и маломощные компьютеры, а главное не скачивать самую последнюю среду разработки и самый последний фреймворк, чтобы написать простой калькулятор :).

Накопив достаточный опыт в предыдущей версии, были понятны основные направления развития и удобства/неудобства пользования библиотекой. Одним из таких недостатков стали визуальные аспекты, стили, поведения, анимации. Тут очень многое было заимствовано из Material Design от компании Google. Например, даже основные цветовые схемы состоят из Primary, Secondary, Surface, Background и других цветов, появилась поддержка векторных шрифтов, некоторые анимации нажатия кнопок, переключателей.

LXUI

Прошли короновирусные времена (а может и не прошли вовсе), библиотека успешно использовалась в нескольких внутренних проектах и возникла мысль, а почему бы не попробовать выложить ее в свет! И тут начался непривычный кошмар :). Регистрации на различных ресурсах, публикации и самое главное документация! С ней сейчас еще предстоит много работы, поэтому сами исходные коды еще не выложены на GitHub. Но уже можно загрузить для испытаний NuGet сборки для Desktop версии (https://www.nuget.org/packages/LXUI.Desktop) и Android (https://www.nuget.org/packages/LXUI.Android). Для простенькой демонстрации был сделан проект Weather (https://github.com/lxuinet/Meteo.Test), который скомпилирован под Android и Desktop (в том числе был опробован в виртуальной машине с операционной системой Raspberry Pi). Дополнительная информация доступна на официальном сайте (http://lxui.net/), сайт к сожалению пока тоже пустой, но надеюсь со временем все наладится!

Weather, Raspberry PiWeather, Raspberry Pi

Hello World!

Итак приступим, попробуем создать простое приложение! Если вы планируете использовать один и тот же код и ресурсы в разных системах (Android/Desktop), то необходимо создать общую библиотеку классов или общий проект. Для создании Desktop версии нам необходимо создать консольный проект или проект Windows Forms, в качестве среды разработки будем использовать Microsoft Visual Studio 2019 (никто не запрещает использовать любую другую среду). Создаем консольный проект .NET Framework версии 4.0 или выше (можно использовать .NET Core проекты)

Создание проектаСоздание проекта

В свойствах проекта изменяем «Тип выходных данных», это нужно чтобы не отображалось окно консоли:

Настройка проектаНастройка проекта

Подключаем NuGet пакет. В поиске вводим LXUI и выбираем LXUI.Desktop:

Добавление NuGet пакетаДобавление NuGet пакета

Теперь необходимо создать стартовую форму, это обычный класс, наследуемый от LX.Control и зададим рамку элемента через свойство BorderSize:

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.BorderSize = 1;
        }
    }
}

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

using LX;
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            App.OnRun += () => new MainForm();
            App.Run();
        }
    }
}

Теперь необходимо добавить нашу форму на экран, для этого у Control есть метод AddToRoot (), вызовем данный метод в конструкторе формы:  

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
        }
    }
}

Запускаем приложение!

Первый запускПервый запуск

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

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;
        }
    }
}

Концепция LXUI — любой элемент может содержать в себе другие элементы и как угодно их выравнивать, например, стать панелью, списком или галерей. Теперь добавим 100 текстовых меток в центре формы и запустим приложение:

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;

            for (int i = 0; i < 100; i++)
            {
                var label = new Label();
                label.Text = "Hello World!";
                label.Alignment = Alignment.Center;
                label.AddTo(this);
            }
        }
    }
}

Вывод текста на экранеВывод текста на экране

Все 100 надписей расположились по центру, давайте превратим нашу форму в список! Для этого используем свойство Layout:

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalList();

            for (int i = 0; i < 100; i++)
            {
                var label = new Label();
                label.Text = "Hello World!";
                label.Alignment = Alignment.Center;
                label.AddTo(this);
            }
        }
    }
}

Вертикальный списокВертикальный список

Можно создавать любые пользовательские Layout и на лету менять поведение элементов внутри формы без изменения разметки формы. Обратите внимание, автоматически создалась полоса прокрутки. В LXUI нет необходимости создать различные ScrollViewer и тому подобные элементы, фреймворк сам определяет их необходимость. Видимость, размеры и цвета элементов прокрутки можно настроить в соответствующих свойствах. На текущий момент нет поддержки колеса мыши, но зато есть так называемая прокрутка «пальцем», привычная всем пользователям тачскринов (к данному подходу очень быстро привыкаешь и в десктопных приложениях).

Теперь добавим иконку справа к нашим надписям. Добавим файл изображения в проект и укажем действие при сборке «Внедренный ресурс». В текущей версии поддерживаются форматы изображений: Bmp, Gif (без анимации), Jpg, Png, Psd, Tga, монохромные векторные изображения, загруженные из шрифтов.

Добавление ресурсов в проектДобавление ресурсов в проект

Загрузим изображение и добавим его в наши надписи:

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalList();

            for (int i = 0; i < 100; i++)
            {
                var label = new Label();
                label.Text = "Hello World!";
                label.Alignment = Alignment.Center;
                label.AddTo(this);

                var image = new PictureBox();
                // Подгоним размер элемента по размеру изображения
                image.AutoSize = true;
                // Расположим элемент справа от текста
                image.Alignment = Alignment.Right;
                // Загрузим изображение из ресурсов
                image.Image = Image.LoadFromResource("*.duck.png");
                // Располагаем по центру элемента
                image.ImageAlignment = Alignment.Center;
                // Добавляем элемент в текстовую метку
                image.AddTo(label);
            }
        }
    }
}

Можно упростить весь код:

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalList();

            for (int i = 0; i < 100; i++)
            {
                var label = this.Add("Hello World!", Alignment.Center);
                var image = label.Add(Image.LoadFromResource("*.duck.png"), Alignment.Right);
            }
        }
    }
}

Текст с картинкойТекст с картинкой

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

using LX;
namespace HelloWorld
{
    public class MainForm : Control
    {
        public MainForm()
        {
        		// Изменение масштаба окна
        		Window.Scale = Scale.Percent200;
        
            this.AddToRoot();
            this.BorderSize = 1;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalList();

            for (int i = 0; i < 100; i++)
            {
                var label = this.Add("Hello World!", Alignment.Center);
                label.Color = Color.Parent.Auto(150);
                label.Shape = CornerShape.Oval;
 								label.Radius = 3;

                var image = label.Add(Image.LoadFromResource("*.duck.png"), Alignment.Right);
            }
        }
    }
}

Изменение масштаба окнаИзменение масштаба окна

Из-за более светлого фона надписи, цвет текста стал черным, это также достигается благодаря указанному по умолчанию цвету текста равному Color.Content. Авто цвета удобно использовать, если вы создаете разные темы, в том числе светлые/темные:

Светлая темаСветлая темаТемная темаТемная тема

Аналоги и тестирование

Существует огромное количество кроссплатформенных GUI, но только не в .NET, реально мало кто назовет даже 1–2 фреймворка. Есть Xamarin.Forms, у которого нет поддержки Linux, тот же .NET MAUI скоро выйдет, опять же без поддержки Linux систем и если честно, подход использовать максимально последний .NET для запуска отпугивает, плюс выходные размеры пустого проекта становятся все больше и больше. Есть AvaloniaUI, которая очень сильно выросла за последние годы и видно была проделана просто гигантская работа в сторону повышения качества продукта. Но это все достаточно крупные игроки, до которых очень далеко. 

Пустой проект на .NET MAUI под Windows 10Пустой проект на .NET MAUI под Windows 10

Поскольку моей задачей является создать очень компактный и очень производительный фреймворк я просто обязан провести некоторые сравнительные тесты. Для этого будем сравнивать LXUI с фреймворками: WPF (Windows Presentation Foundation, аналог Windows Forms от Microsoft) и AvaloniaUI (кроссплатформенный фреймворк), к сожалению MAUI даже не удалось запустить, поэтому пока их исключаем из тестов. Для меня важными критериями в этих тестах являются:

  1. Время запуска

  2. Расход памяти

  3. Общая производительность и плавность интерфейса

Конфигурация тестовой среды:

  • Процессор: Intel Xeon E5–2696 v3

  • Оперативная память: 64 Гб

  • Видеокарта: ASUS GeForce GTX 1660 Ti 6 Гб

  • Операционная система: Window 10 Pro 21H2

  • Среда разработки: Visual Studio Community 2019 Версия 16.11.15

Код тестовых примеров я буду приводить на LXUI, код других тестовых проектов можно загрузить с GitHub (https://github.com/lxuinet/LX.Test). Сразу оговорюсь, возможно, я не правильно (не оптимально) составлял тестовые примеры, надеюсь, в комментариях мне предложат другие варианты, более быстрые. Также не ставится целью кого-то обидеть или принизить, любая здравая критика только приветствуется. Поехали!

Test 1. Проверяем скорость загрузки большого числа элементов

Задача. Создать форму с вертикальным списком и прокруткой, в который добавить N-ое количество кнопок с текстом и изображением, с автоматическим размером по содержимому. 

using LX;
namespace Test1.LX
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.Color = Color.Surface;
            this.Padding = 8;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalList(8);

            var source = Image.LoadFromFile("test.png");
            for (int i = 0; i < 100000; i++)
            {
                var button = new Button();
                button.Color = Color.LightGray;
                button.Layout = VerticalList.Default;
                button.AddTo(this, Alignment.TopCenter);

                var label1 = button.Add("Top " + i.ToString(), Alignment.TopRight);
                var image1 = button.Add(source, Alignment.TopCenter);
                var label2 = button.Add("Bottom " + i.ToString(), Alignment.TopLeft);
            }
        }
    }
}

Тест 1Тест 1

WPF (N = 10 000 элементов)

  • Время запуска: ~23 c

  • Расход памяти: ~500 Мб

  • Количество сборок мусора: ~30

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~3 c

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

Диагностика Test1.WPFДиагностика Test1.WPF

AvaloniaUI (N = 10 000 элементов)

  • Время запуска: ~24 c.

  • Расход памяти: ~950 Мб

  • Количество сборок мусора: ~30

  • Прокрутка списка: Практически недоступна

  • Изменение размера окна: ~12 c

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

Диагностика Test1.AvaloniaUIДиагностика Test1.AvaloniaUI

LXUI (N = 100 000 элементов) в 10 раз больше элементов

  • Время запуска: ~4 c

  • Расход памяти: ~550 Мб

  • Количество сборок мусора: ~10

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~100 мс

Нет никаких задержек при прорисовке, минимальные задержки при изменении размера окна.

Диагностика Test1.LXUIДиагностика Test1.LXUI

Test 2. Создаем галерею иконок

Задача. Загрузить из папки 226 изображения, в формате PNG. Создать форму с галереей изображений и добавить в нее 226 х N элементов. 

using System;
using System.IO;
using System.Linq;
using LX;

namespace Test2.LX
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.Color = Color.Surface;
            this.Alignment = Alignment.Fill;
            this.Layout = new VerticalGallery();

            var list = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory + "png-256", "*.png")
            										.Select(file => Image.LoadFromFile(file))
            										.ToList();

            for (int i = 0; i < 1000; i++)
            {
                list.ForEach(source =>
                {
                    var image = Add(source);
                    image.ImageAlignment = Alignment.Zoom;
                    image.Size = 128;
                });
            }
        }
    }
}

Тест 2Тест 2

WPF (N = 100, 22 600 элементов)

  • Время запуска: ~3 c

  • Расход памяти: ~300 Мб

  • Количество сборок мусора: ~5

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~100 мс

Нет никаких задержек при прорисовке, минимальные задержки при изменении размера окна.

Диагностика Test2.WPFДиагностика Test2.WPF

AvaloniaUI (N = 100, 22 600 элементов)

  • Время запуска: ~4 c

  • Расход памяти: ~250 Мб

  • Количество сборок мусора: ~2

  • Прокрутка списка: Практически недоступна

  • Изменение размера окна: ~4 c

Прокрутку сделать практически невозможно, как и изменение размера окна.

Диагностика Test2.AvaloniaUIДиагностика Test2.AvaloniaUI

LXUI (N = 1000, 226 000 элементов) в 10 раз больше элементов

  • Время запуска: ~2 c

  • Расход памяти: ~350 Мб

  • Количество сборок мусора: 0

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~100 мс

Нет никаких задержек при прорисовке, минимальные задержки при изменении размера окна.

Диагностика Test2.LXUIДиагностика Test2.LXUI

Test 3. Работа с текстом

Задача. Создать форму с текстовым редактором, загрузить большой текст ~17 х N символов и выровнять по правому краю. 

using LX;
namespace Test3.LX
{
    public class MainForm : Control
    {
        public MainForm()
        {
            this.AddToRoot();
            this.Color = Color.Surface;
            this.Alignment = Alignment.Fill;

            var text = new TextBox();
            text.InlineAlignment = InlineAlignment.Far;
            text.VerticalScrollBar.Visible = true;
            text.MultiLine = true;
            text.WordWrap = true;
            text.Text = TextGenerator.Create(100000);
            text.AddTo(this, Alignment.Fill);
        }
    }
}

Тест 3Тест 3

WPF (N = 10 000, 168 890 символов)

  • Время запуска: ~1.5 c

  • Расход памяти: ~100 Мб

  • Количество сборок мусора: 0

  • Прокрутка текста: Очень быстро

  • Изменение размера окна: ~1 с

  • Задержка при вводе: ~1 с

Большая задержка при вводе текста и изменении размера окна.

Диагностика Test3.WPFДиагностика Test3.WPF

AvaloniaUI (N = 10 000, 168 890 символов)

  • Время запуска: ~4 c

  • Расход памяти: ~150 Мб

  • Количество сборок мусора: > 100

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~2 c

  • Задержка при вводе: ~2 c

Большая задержка при вводе текста и изменении размера окна.

Диагностика Test3.AvaloniaUIДиагностика Test3.AvaloniaUI

LXUI (N = 100 000, 1 788 890 символов) в 10 раз больше символов

  • Время запуска: ~0.5 c

  • Расход памяти: ~200 Мб

  • Количество сборок мусора: 3

  • Прокрутка списка: Очень быстро

  • Изменение размера окна: ~0 с

  • Задержка при вводе: ~0 c

Нет никаких задержек при прорисовке, изменений размера окна и вводе символов, максимальная плавность.

Диагностика Test3.LXUIДиагностика Test3.LXUI

Итоги тестирования 

По результатам проведенных тестов, LXUI имеет очень высокую оптимизацию производительности, более чем в 10 раз превосходящую среди сравниваемых фреймворков. Данный запас производительности дает гарантию быстрого и плавного исполнения приложений в мобильных устройствах и более слабых настольных системах. WPF достаточно неплохо справилась с данными тестами, в основном были задержки при изменении размеров окна, где происходит основной перерасчет положения элементов управления. У AvaloniaUI есть заметные проблемы с производительностью, что не может сказаться на выполнении тяжелых пользовательских интерфейсов, особенно на мобильных устройствах. Конечно, это были синтетические тесты и нужно учитывать множество факторов, например, сейчас LXUI проигрывает в скорости загрузки больших изображений (4к и выше), также требуется много и других доработок.

Вывод

На мой взгляд, LXUI мог бы стать вполне конкурентным фреймворком для построения пользовательских кроссплатформенных систем. Основным преимуществом я бы назвал:

  1. Маленький размер выходных файлов, всего одна библиотека размером в 3 МБ.

  2. Высокая оптимизация и производительность.

  3. Простой вход для новичков, не нужно изучать язык разметки XAML и т.п. и в тоже время структура библиотеки схожа с классическим Windows Forms

  4. Поддержка устаревших версий .NET, сред разработки и операционных систем, например в системе Linux достаточно установить пакеты mono-complete, libsdl2 и libsdl2-ttf, которые входят в официальные репозитории даже на таких система как Raspberry Pi.

В перспективе необходимо расширять библиотеку пользовательских элементов управления, скинов, сделать её максимально потокобезопасной, добавить поддержку векторной графики, проигрывания звука и видео, расширить список платформ, в частности планируется добавить поддержку iOS и macOS, дать разработчикам больше доступа к графической подсистеме, добавить 3D, шейдеры, а главное привести в порядок документацию, добавить больше примеров и сделать полноценный конструктор форм и пользовательских элементов управления. Одно из направлений можно развить в сторону игр, так как большинство казуальных игр строятся на GUI со стандартными элементами управления. 

Текущая статья очень кратко описывает возможности LXUI, не затронуты многие темы, например: скины, таймеры, горячие клавиши, анимации. 

P.S.

Это моя первая статья такого рода, приношу извинения за возможные многочисленные ошибки, жду от вас отзывов и рекомендаций, до скорой встречи!

Ссылки

Основные ресурсы:

Используемые в статье источники:

© Habrahabr.ru