Кроссплатформенная новогодняя демка на .NET Core и Avalonia

    "ААА! Пришло время переписывать на .NET Coreǃ", говорили они, WPF в комментариях обсуждали. Так давайте же проверим, можно ли написать кросс-платформенное GUI приложение на .NET / C#.



    Новогоднее настроение навеяло идею сделать анимацию падающего снега. Были такие демки под DOS, горящий огонь, фракталы, снежок, падающий на ёлочку, и так далее.


    Как увидим ниже, это не только весело, но и позволит испытать ключевой функционал UI фреймворка. Поехали!


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


    Для Avalonia есть Visual Studio Extension с шаблоном проекта. Устанавливаем, создаём Avalonia .NET Core Application. Видим привычные по WPF App.xaml и MainWindow.xaml. Однако, проект содержит <TargetFrameworks>netcoreapp1.1;net461</TargetFrameworks>, меняем на <TargetFramework>netcoreapp2.0</TargetFramework>, мы же не в каменном веке.


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


    В остальном у нас в руках привычный XAML с привычными контролами и пропертями. Обо всех отличиях можно почитать в документации.


    Для произвольного рисования существует одноимённый аналог WriteableBitmap из WPF. Огромный плюс в том, что нет проблем рисовать в нём из любого потока, выглядит это так:


    <Image Source="{Binding Bitmap}" Stretch="Fill" />

    using (ILockedFramebuffer buf = writeableBitmap.Lock())
    {
        uint* ptr = (uint*) buf.Address;
    
       // Рисуем
        *ptr = uint.MaxValue;
    }

    Однако Image, который привязан к нашему writeableBitmap, не обновится сам по себе, ему необходимо сказать InvalidateVisual().


    Таким образом, мы можем рисовать анимацию в фоновом потоке, не нагружая UI thread. Помимо Image добавим пару слайдеров для управления скоростью падения снега и количеством снежинок, здесь всё стандартно, {Binding Mode=TwoWay}. Плюс кнопка "начать заново", тоже стандартная привязка к ICommand. Замечу, что использованы векторные иконки на XAML, скопипащенные из гугла, <Path> фунциклирует как положено.


    Разметка целиком: MainWindow.xaml


    Снежный алгоритм


    "Физика"


    Двигаем снежинку вниз на один пиксель. Если пиксель уже занят "лежащей" снежинкой, проверяем точки слева и справа, и двигаем туда, где свободно. Всё занято — помечаем точку как "лежащую". Таким образом достигается скатывание снежинок с наклонных поверхностей.


    Параллакс


    Для достижения объёмного эффекта зададим каждой снежинке рандомную скорость. Чем скорость ниже, тем более тёмный оттенок используем для рисования.


    Чтобы наша "физика" работала корректно, необходимо перемещать снежинки не более, чем на 1 пиксель за кадр. То есть самые быстрые снежинки двигаются каждый кадр на пиксель, остальные — пропускают некоторые кадры. Для этого можно применить float координаты и просто перерисовывать каждую снежинку на каждый кадр. Вместо этого я использую два целочисленных short поля и перерисовываю снежинку только если она реально сдвинулась.


    Рендеринг


    Основная идея — избежать полной перерисовки кадра. Нам надо как-то хранить "лежащий" снег, нарисованные пользователем точки, загруженные изображения (да, можно рисовать мышью и грузить пикчи правым кликом — снежок будет прилипать к ёлочкам и надписям).


    Простое и эффективное решение — использовать сам WriteableBitmap. "Перманентные" пиксели пусть будут полностью непрозрачными (A = 255), а для движущихся снежинок A = 254.


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


    Как это запустить?


    Благодаря возможности рисования прямо "по живому" получилась доволно залипательная штука, попробуйте :)


    Для всех ОС инструкция одинаковая:


    • Установить .NET Core SDK
    • git clone https://github.com/ptupitsyn/let-it-snow.git
    • cd let-it-snow/AvaloniaCoreSnow
    • dotnet run

    Заключение


    .NET Core молод, Avalonia ещё в альфе, но уже сейчас эти инструменты решают поставленную задачу! Код простой и понятный, никаких хаков и лишних приседаний, прекрасно работает на Windows, macOS, Linux.


    Альтернативы?


    • Qt (посложнее будет в использовании)
    • Java (нет нормального unsafe)
    • Electron (JavaScript + HTML — нет уж, спасибо)

    UI в данной демке очень прост, но он использует несколько наиболее важных фич:


    • Layout (Grid, StackPanel, выравнивание) — основа вёрстки
    • Binding (привязка контролов к свойствам модели)
    • ItemsControl, ItemsPanel, DataTemplate — работа с коллекциями данных
    • WriteableBitmap — прямая работа с изображениями
    • OpenFileDialog, нативный на каждой платформе

    Этого уже достаточно, чтобы построить UI любой сложности. Так что можем сказать, что закрыт последний пробел в экосистеме .NET: есть возможность создавать веб (ASP.NET Core), мобильные (Xamarin) и десктопные (Avalonia) приложения, при этом переиспользовать код, располагая его в библиотеках .NET Standard.


    Ссылки


    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 55
      –4
      А как же PyQt? Вроде довольно неплох.
        –1
        PyQt это опять же — Qt. Лично по мне так он довольно сложен для понимания, особенно QML. Все-таки самый лучший способ писать интерфейс разметкой — это xml (и его расширения типа XAML и т.д.). А вот в json разметку еще и намешанную с js воспринимать очень тяжело.
          +4
          XML легче читать чем json… Вы это серьезно?
            +10

            В JSON даже комментарии нельзя вставлять, не говоря уже о схемах и прочем.
            Для передачи данных да, он компактнее и, может, более читаем. Но для разметки, конфигов я предпочту XML.

              +4
              В QML-то есть комментарии, безо всяких извращений вида <!--… -->
                –2
                Комментарий в JSON — {...«node»:«нечто по этому узлу», «nodeCOMMENT»:«Комментарий»...}
                  –1
                  это не комментарий
                +7

                У JSON в плане использования как языка разметки есть фатальные недостатки — неименованые типы объектов и отсутствие встроенных текстовых нод. Из-за этого при определении списка элементов приходится писать что-то типа


                {
                  "@type": "StackPanel",
                  "children": [
                    {
                      "@type": "TextBlock",
                      "text": "Hello world"
                    },
                    {
                      "@type": "Button",
                      "text": "Click me"
                    }
                  ]
                }

                вместо


                <StackPanel>
                  <TextBlock>Hello world</TextBlock>
                  <Button Text="Click me"/>
                </StackPanel>
                  +2

                  В QML это будет


                  ColumnLayout {
                     Text {
                       text: qsTr("Hello world")
                     }
                     Button {
                       text: qsTr("Click me")
                     }
                  }

                  Единственный косяк QML — возможность писать код прямо в разметке — так и поощряет быдлокодить и приходится каждый раз себя бить по рукам для разделения вида контролов и их логики

                    +4
                    Это выглядит лучше чем json, но я бы не сказал, что лучше, чем XAML. Примерно одинаково и дело привычки. (хотя я бы в вашем примере еще комментировал закрывающие скобки, если большая вложенность)
                      0

                      QML позволяет быдлокодить примерно так:


                      Button {
                           property int times = 0
                           text: qsTr("Clicked %1 times").arg(times)
                           onClicked: times++
                      }

                      Что в XAML получится кудааа длиннее. Но в то же время WPF заставит сделать аккуратно, а QML на все это пофиг. Поэтому от мало-мальски сложного проекта на QtQuick начинают течь слезы, если только разработчик не заставил себя следовать какой-нибудь методологии (вроде Flux)

                        +1
                        В UWP Можно сделать примерно так:
                          <Button Click="Onclick" Content="{x:Bind GetClickCount()}"/>
                        

                        А логику уже в codebehind, если это логика UI. Ну или во вьюмодель если бизнес.

                        Кстати, в андроидовском axml можно тоже в биндингах делать некую логику. Отличная замена конвертерам.

                        Но вот полностью функции писать в верстке. Сомнительное это преимущество.
                          0
                          Но вот полностью функции писать в верстке. Сомнительное это преимущество.

                          Так я его в недостатки записал

                          0
                          Поэтому от мало-мальски сложного проекта на QtQuick начинают течь слезы, если только разработчик не заставил себя следовать какой-нибудь методологии

                          QML — это всего лишь GUI, и javascript там нужен для реализации логики GUI. Бизнес-логика должна реализовываться отдельно. Имо если в проекте много говнокода, слезы потекут независимо от того, на чем он написан
                      0

                      Тут на самом деле есть один тонкий момент — никто не запрещает написать так:


                      {
                          "StackPanel": 
                          {
                              "TextBlock": "Hello world",
                              "Button":
                              {
                                  "Text": "Click me"
                              }
                          }
                      }

                      но тут встает вопрос с повторением свойств, например:


                      {
                          "StackPanel": 
                          {
                              "TextBlock": "Hello world",
                              "Button":
                              {
                                  "Text": "Click me"
                              },
                              "Button":
                              {
                                  "Text": "And me!"
                              }
                          }
                      }

                      Парсеры, тот же JSON.parse, при повторении свойства вернет значение именно последнего свойства (работает аналогично инициализации объекта), а вот при потоковом парсинге (например с помощью JsonTextReader из Json.NET) мы увидим все свойства, даже повторяющиеся и можем их корректно обработать. Но это очень холиварный момент. Очень.

                      +7
                      Пока я из XML читал только XAML и ATOM мне всегда было не ясно, как JSON может быть более читаем: он менее строгий и ограничен единственной конструкцией Ключ-Значение, которой описывается и свойства и иерархия. Когда у xml свойства могут быть атрибутами, а иерархия это всегда вложенные теги.

                      Потом я уже столкнулся с plist. Когда xml используется по json-идеалогии. тогда его из-за излишней многословности читать действительно сложнее.

                      Воспользуюсь примером kekekeks:
                      Если сравнивать такой json:
                      {
                        "@type": "StackPanel",
                        "children": [
                          {
                            "@type": "TextBlock",
                            "text": "Hello world"
                          },
                          {
                            "@type": "Button",
                            "text": "Click me"
                          }
                        ]
                      }
                      

                      И XML написанный в том же стиле (примерно как plist):
                      <panel>
                        <type>StackPanel</type>
                        <children>
                          <control>
                            <type>TextBlock</type>
                            <text>Hello world</text>
                          </control>
                          <control>
                            <type>Button</type>
                            <text>Click me</text>
                          </control>
                        </children>
                      </panel>
                      

                      То, конечно, json выигрывает. Но нормальный XML намного удобнее:
                      <StackPanel>
                        <TextBlock>Hello world</TextBlock>
                        <Button Text="Click me"/>
                      </StackPanel>
                      


                      Тут тег однозначно сопоставляется с экземпляром объекта, свойства объекта с атрибутами. Иерархия тоже легче читается.
                      –5
                      По выразительности ни XML, ни JSON не сравнится с QML.
                        +1

                        Любителям синтаксиса QML рекомендую посмотреть в сторону AmmyUI.

                    +1
                    Напомнило демку с диска к книге Страуструпа за 99 год вроде. Там аналогичный эффект был, правда без паралакса и под DOS. Хороший был диск. А сколько там было исходников вирусни того времени — ух…
                      +2
                      Ухты. А как Avalonia это делает? Dotnet core — он же GUI биндингов не имеет? Они что, через интроспекцию лезут напрямую в ось, в рантайме проверяют что за ось и в рантайме же биндятся к нужным GUI апишкам?
                        +2

                        Вот кое-что есть на хабре: https://habrahabr.ru/post/328684/

                          +4

                          Там всё достаточно примитивно в плане детекта — RuntimePlatform.GetRuntimeInfo().OperatingSystem выдаёт текущую операционку. А дальше создаются окошки уже через P/Invoke либо к Win32 API, либо к GTK3, либо к Cocoa.


                          Подробнее по архитектуре см. с 15:00 здесь

                            –1
                            Ага, то есть «P/Invoke». Неторопливая штука в целом. Не факт, что быстрее Electron.
                              +8

                              Вы точно понимаете, как работает P/Invoke? Вы точно понимаете, как именно работает электрон и почему именно он тормозит?

                                –1
                                Если .net хочет вызвать, к примеру, «SendMessageW» на винде, то она делает преобразования для всех аргументов (потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки), потом вызов, потом преобразование возвращаемых знаечений, если есть. Electron — это просто Chromium. В нем оптимизированный движок для рендеринга HTML, он компилирует JS в нативный код, все взаимодействие между скомпилированным JS и HTML бесшовно.

                                Но специально я не сравнивал — любопытствую. Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code. Да, воторой запуск такой же медленный, как первый :)
                                  +6
                                  потому что строка в .net и null-terminated UCS-2 в WinAPI — это разные штуки

                                  На самом деле нет, дотнетные строки нуль-терминированы, вы можете взять указатель и передать как есть. Да, в ряде случаев маршалинг нужен, но это контролируемый процесс. Так, всё взаимодействие с DirectX у нас идёт через msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований.


                                  Демка из статьи выше у меня запустилась сильно медленнее, чем стартует Visual Studio Code

                                  А вы её запускали через dotnet run? Он каждый раз дёргает MSBuild и кучу ненужностей, чтобы определить, надо ли пересобрать проект (и обычно пересобирает даже тогда, когда не надо). Процесс этот не быстрый. Сделайте dotnet publish под нужную платформу и уже собранный релизный вариант запустите.

                                    +2
                                    msil-инструкцию calli, которая выполняет прямой вызов по указателю без автоматических преобразований

                                    Интересно, а где можно на это посмотреть?

                                      +3

                                      См. SharpGenTools, которым обрабатывается используемый нами SharpDX в процессе сборки. Мы себя отдельно SharpGenTools используем в AvalonStudio для генерации кода интеропа с libdbgshim/ICorDebug для отладки кода под .NET Core.

                                +6

                                "Неторопливая" — очень относительное понятие. Да, PInvoke медленный по сравнению с прямым вызовом функции, он добавляет от 10 до 30 инструкций (см MSDN). Но надо понимать, что эта разница — порядка наносекунд, и в случае вызова функций сложнее a+b просто теряется. Например, Ignite.NET работает с JVM через PInvoke, и тормозов от этого не испытывает.


                                Electron:


                                • тащит за собой целый браузер, жрёт немеряно памяти и создаёт новый процесс на каждый чих
                                • C# быстрее JavaScript и лучше во всех отношениях
                                • XAML лучше HTML

                                Другое дело, что layout & rendering в Electron действительно может быть быстрее для некоторых сценариев, браузерный движок очень круто заоптимизирован под это дело.

                                  +3
                                  layout & rendering в Electron действительно может быть быстрее

                                  Так рендерилку (Skia) мы у них в цельнотянутом виде утащили. На не-windows-платформах используется.

                            +2

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

                              +7

                              Спасибо за статью и рекламу Avalonia! Очень нравится концепция, надеюсь выстрелит.

                                +2

                                Классический эффект ранних интро.
                                Единственное — в этом эффекте у снежинок обычно добавляли небольшие флуктуации по горизонтали — как это и выглядит в натуре, если нет ветра.
                                Добавил эту фичу и сделал pull-request: https://github.com/ptupitsyn/let-it-snow/pull/1/commits

                                  +2
                                  Я бы посмотрел ответочку от

                                  • Java
                                  • Kotlin
                                  • WebAssembly? O_o
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    Господа, а нормальный-то WPF не планируется к .net core что ли?
                                      +1

                                      Нет. Вместо него поверх CoreCLR работает UWP. Который нигде кроме Windows 10 работать не планирует.

                                      • НЛО прилетело и опубликовало эту надпись здесь
                                          0

                                          Где? В .NET Core? Или поддержка вайном UWP? Первое технически слабореализуемо (Mono уже пытались когда-то давно реализовать поддержку WinForms поверх вайна, получилось плохо), во второе слабо верится, ввиду общей непопулярности UWP как платформы.

                                          • НЛО прилетело и опубликовало эту надпись здесь
                                              0

                                              Они его продвигать могут сколько угодно, но разработчиков под UWP от этого не прибавится. По фичам он проигрывает WPF всухую, а единственную платформу, ради которой это дело хоть кто-то использовал, благополучно похоронили.

                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                  0
                                                  Под win S запускаются только приложения рз маркета.

                                                  Через Desktop Bridge на этой Win S хоть Windows Forms можно запустить.

                                                  0
                                                  По фичам он проигрывает WPF всухую

                                                  Ну нет же (в смысле не всухую). Все новые фичи API делаются в WinRT и в WPF доступны только через костыли в виде Desctop Bridge.

                                                  Те фичи, что не поддерживаются в WinRT либо в планах, либо под вопросом существования (как например трей).
                                                    +1

                                                    Grid.SharedSizeGroup завезли уже? А свои MarkupExtension уже можно делать? Уже сколько лет технологии, а даже таких базовых вещей нет.


                                                    А WinRT-only-фичи — это какое-нибудь живое обновление плиток, которое нужно мессенджерам и приложениям по доставке пиццы.

                                                      0
                                                      Вы хотите передергивать, или объективности?
                                                      Если передергивать, то кому нужны все эти Grid.SharedSizeGroup и MarkupExtension?

                                                      В WPF завезли нормальную поддержку DPI и UI Адаптивный под разные инструменты ввода? Даже таких базовых вещей нет.

                                                      А WinRT-only-фичи, кроме вами озвученных — пуш уведомления, поддержка экстеншенов из коробки, нормальная поддержка HDPI, SvgImageSource, Composition API, поддержка windows timeline из коробки, пакетная упаковка приложений (аля мак ос).
                                                        +2
                                                        кому нужны все эти Grid.SharedSizeGroup и MarkupExtension

                                                        Нужны при разработке чего-то более сложного, чем приложение для доставки пиццы. Свои markup extensions даже в Xamarin.Forms есть.


                                                        В WPF завезли нормальную поддержку DPI и UI Адаптивный под разные инструменты ввода

                                                        DPI всегда вполне вменяемо поддерживался. В 4.6.2 прикрутили поддержку per-monitor DPI. С DPI проблемы были не в WPF, а в винформах, ибо винформы базируются на технологиях из 90-х


                                                        пуш уведомления
                                                        пакетная упаковка приложений (аля мак ос).

                                                        Делается через desktop bridge, к технологии отображения окон на экран отношения не имеет.

                                                          0
                                                          Мне прям интересно, что это за приложения такие, серьёзность которых определяется использованием SharedGridSize. (до WPF вообще серьёзных приложений не писали, ага)

                                                          Ок, продолжу ваше ребячество:
                                                          Ничего сложнее «приложения по доставке пиццы» нельзя написать не используя RelativePanel, AdaptiveTrigger и условного XAML (последний «даже в Xamarin.Forms есть» в каком-то виде).

                                                          В 4.6.2 прикрутили поддержку per-monitor DPI.
                                                          Ага бекпортировали из UWP в технологии 00-х.

                                                          Делается через desktop bridge

                                                          Как я выше и писал, через костыли в виде Desktop to UWP Bridge. Тогда только Классическим (аккуратное слово, означающее то же, что и легаси в общем-то) приложениям разрешают доступ до современного WinRT API.
                                                            0

                                                            AdaptiveTrigger реализуется средствами WPF посредством биндинга к ActualWidth и конвертера. RelativePanel при желании можно реализовать самостоятельно, более-менее работающая есть на гитхабе.
                                                            Условный XAML делается на уровне своего markup extension (как это сделано в Xamarin.Forms)

                                                              0
                                                              А SharedGridSize это такая незаменимая сверхмагия разработанная надмозгом?
                                                                0
                                                                Ну и услвный XAML в XF выглядит так:
                                                                <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                                                                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                                                                        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
                                                                        xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
                                                                        xmlns:formsAndroid="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.Platform.Android;targetPlatform=Android"
                                                                        xmlns:win="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255,
                                                                            Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
                                                                        x:Class="NativeViews.NativeViewDemo">
                                                                    <StackLayout Margin="20">
                                                                        <ios:UILabel Text="Hello World" TextColor="{x:Static ios:UIColor.Red}" View.HorizontalOptions="Start" />
                                                                        <androidWidget:TextView Text="Hello World" x:Arguments="{x:Static formsandroid:Forms.Context}" />
                                                                        <win:TextBlock Text="Hello World" />
                                                                    </StackLayout>
                                                                </ContentPage>
                                          0

                                          А расскажите что мешает например сделать биндинги к Qt(зачем ещё одна графическая либа)? Известный фреймворк, сделан профессионально, из коробки до кучи фич от лайв редактирования до трансляции в браузер. Кастомизируется по самое нехочу. Отличная лицензия. Из недостатков только что веб в один клик не запилить(хотя зная современный веб это можно считать плюсом) и что заставляет под себя подстраиваться (хотелось бы более универсальное как стандартная библиотеки, чтобы не тянуть зависимости ядра, а сразу заюзал функционал в любой проект без остального qt).

                                            +5

                                            Дата-биндингов нормальных нет, шаблонов нормальных нет, lookless-контролов нет, т. е. идеологически Qt застрял где-то во временах Windows Forms и Delphi 7. А так хороший фреймворк, никто не спорит.

                                            +2

                                            Здорово! Приятно видеть, что UI dotNet с XAML можно запускать на этих платформах.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое